IOP _ ThunderNFT 34958 - [Smart Contract - Critical] Incorrect Setting of Amount in ExecutionResult

Submitted on Sun Sep 01 2024 20:25:53 GMT-0400 (Atlantic Standard Time) by @anatomist for IOP | ThunderNFT

Report ID: #34958

Report type: Smart Contract

Report severity: Critical

Target: https://github.com/ThunderFuel/smart-contracts/tree/main/contracts-v1/libraries

Impacts:

  • 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.

References

https://github.com/ThunderFuel/smart-contracts/blob/260c9859e2cd28c188e8f6283469bcf57c9347de/contracts-v1/libraries/src/execution_result.sw#L31 https://github.com/ThunderFuel/smart-contracts/blob/260c9859e2cd28c188e8f6283469bcf57c9347de/contracts-v1/execution_strategies/strategy_fixed_price_sale/src/main.sw#L146 https://github.com/ThunderFuel/smart-contracts/blob/260c9859e2cd28c188e8f6283469bcf57c9347de/contracts-v1/thunder_exchange/src/main.sw#L404

Proof of concept

POC

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.

Attacker Account

  • Account(fuel): fuel16r69m48pwg4c8dtlnpze2m95t2fws4vwdju7w7sm9ztj459c0ekqs6xjg5

  • Account: 0xd0f45dd4e1722b83b57f9845956cb45a92e8558e6cb9e77a1b28972ad0b87e6c

  • Private Key: 306a75f0093834948e363ece5ba1b5a7eaad99f2fc9ab976ba01c2dbea3320f6

Admin Account

  • Account(fuel): fuel1gq9fak7588gs0hk8yuun95hpmaahldy6204h2v37945kazug46mqz682vm

  • Account: 0x400a9edbd439d107dec7273932d2e1df7b7fb49a53eb75323e2d696e8b88aeb6

  • Private Key: a710552d8d29a0f7aefcb412399fbb041a31ae155cb0eb95d370bf8f99f0409f

Local Node Setup

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.

Last updated

Was this helpful?