Direct theft of any user NFTs, whether at-rest or in-motion, other than unclaimed royalties
Description
Brief/Intro
The ExecutionResult in the libraries incorrectly sets the amount to a constant value of 1 in the s1 function. If a victim places a buy-side order with an amount greater than 1, the order is placed successfully. However, after execution, the amount is incorrectly changed to 1, allowing the attacker to fulfill the order with only 1 asset, resulting in a loss for the victim.
Vulnerability Details
When Thunder Exchange executes an sell-side taker order, it receives the execution result from strategy.execute_order(order). This function finds the matched order and generates the execution result. During this process, several checks are performed, but the amount is not properly validated. Instead, the amount is set to a constant value of 1, regardless of the original order's amount. When Thunder Exchange receives the execution result, it checks if the asset and amount provided by the taker match those in the execution result, which incorrectly shows an amount of 1. This allows the taker to fulfill the order with just 1 corresponding asset, disregarding the original order's amount.
A prerequisite for this attack is that victim must placed a buy-side order with amount greater than 1. In Fuel, NFTs differ from those in Ethereum as they are native assets, blurring the boundary between NFTs and fungible tokens (FTs). This ambiguity makes it plausible that users might use this protocol to sell FTs. Furthermore, fractional NFTs exist in Ethereum, so we can't strongly assert that there is only one NFT for each asset (contract, token_id pair). Therefore, this scenario is highly likely to occur.
Impact Details
An attacker can take any buy-side order by providing only 1 asset, even if the original order required a larger quantity. This results in the victim not receiving the expected amount of assets they ordered.
This PoC demonstrates a scenario where a victim places a buy-side order with a price of 10, demanding 10 base assets, which should typically result in no loss and no profit for the victim. However, as shown in this PoC, an attacker can take this order by providing only 1 base asset, causing the victim to lose 9 base assets.
Prerequisite
To demonstrate the impact, we need to set up two accounts: an admin account, which will set up the Thunder Exchange contract and also act as the victim placing an order, and an attacker account, which will later take the order with less asset amount than required by the victim.
This PoC uses commit a9e5e89f5fb38e3b3d6e6bdfe1ad339a01f2f3b9 of Fuel-Core.
Modify state_config.json to allocate initial funds to the above two accounts:
After building the local node with cargo build --release, run the node with:
Client Patch
This PoC uses commit efda0397c7bee77de73bd726ec0b732d57614973 of Sway.
I used the forc-run client to deploy the contract and run the PoC script. Since adding an output address directly isn't straightforward, I patched the client as follows:
Deploying Thunder Exchange Contract
Use the admin account to deploy all the contracts listed below and retrieve their respective contract addresses.
Run the following command in each contract's directory to deploy the contracts:
Here's an example output for subsequent reference:
Setup Script
This script sets up the Thunder Exchange protocol using the admin's account, and also the admin acount will act as victim account, who place a buy-side order to buy 10 base asset with price 10.
The collection (0x7e2becd64cd598da59b4d1064b711661898656c6b1f4918a787156b8965dc83c) and token_id (0) correspond to the default base asset (f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07).
Note that the deployed contract addresses are hardcoded in the main function; you will need to change these to your own deployed contract addresses.
Forc.toml
src/main.sw
Running Setup Script
Use the following command with the patched forc-run and the admin account's private key to execute the setup script.
Note that we are using the contract addresses from the deployment output above.
Upon running the setup script, you should see an output similar to the following:
From the log output, we retrieve the victim's account and nonce, which are:
After running the setup script, the account balances of the attacker and victim are as follows: both accounts initially have 1152921504606846976. The victim placed a buy order with a price of 10, reducing the victim's balance to 1152921504606846966.
Attacker Script
This script demonstrates how an attacker can take the previously placed order by the victim, providing only 1 base asset instead of the 10 base assets required by the victim in their buy order.
The collection (0x7e2becd64cd598da59b4d1064b711661898656c6b1f4918a787156b8965dc83c) and token_id (0) correspond to the default base asset (f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07).
Note the deployed contract addresses are hardcoded in the main function, so you'll need to replace them with your own deployed contract addresses. Additionally, the victim's address and nonce are taken from the previous output; make sure to update these values accordingly.
Forc.toml
src/main.sw
Running Attacker Script
Before running the attacker script, we need to patch contracts-v1/libraries/src/order_types.sw. Making ExtraParams public will simplify our exploit script.
Use the following command with the patched forc-run and the attacker account's private key to execute the attack script.
Note that we are using the contract addresses from the deployment output above.
Upon running the attack script, you should see an output similar to the following:
After running the attacker script, we can see that the order is successfully taken with only 1 base asset. As a result, the attacker's balance increases by 9 to 1152921504606846985, while the victim loses 9 base assets.