Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

[M-1] Usage of non-standard ERC20 tokens may loss of funds

[M-1] Usage of non-standard ERC20 tokens may loss of funds

Description

The collectFee() function collects all WETH tokens and native ETH held by the contract and sends them to the collector address. It assumes that the full balance of WETH can be successfully transferred to s_collector using the standard transfer() method.

However, the function does not account for weird, non-standard ERC20 tokens which may behave differently. These include:

  • Fee-on-Transfer Tokens which deduct a fee during transfer,

  • Rebase Tokens which automatically adjust balances,

  • ERC777 Tokens which can trigger hooks and reentrancy,

  • Blacklisting Tokens which may restrict transfers based on the sender/recipient.

function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
@> i_weth.transfer(s_collector, collection);
(bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
}

Risk

Likelihood:

  • When the token address given as WETH is non-standard (e.g., a fee-on-transfer token or ERC777),

  • When s_collector is blacklisted or recipient balance changes due to a rebase mid-transaction,

Impact:

  • Loss of funds due to partial or failed transfers that go unnoticed,

  • Reentrancy vulnerability via ERC777 hooks,

  • Failed transactions due to transfer restrictions or changes in balance logic.

Proof of Concept

  • Fee-on-transfer token: Only amount - fee is received by s_collector, meaning i_weth.balanceOf is not equal to transferred amount. Silent fund loss.

function transfer(address recipient, uint256 amount) external returns (bool) {
uint256 fee = amount / 100; // 1% fee
_transfer(msg.sender, feeReceiver, fee);
_transfer(msg.sender, recipient, amount - fee);
return true;
}
  • Rebase token: Balances can change between the balanceOf call and the transfer() call. Can result in over/under sending.

function rebase(uint256 epoch, int256 supplyDelta) external {
if (supplyDelta == 0) return;
totalSupply = totalSupply + uint256(supplyDelta);
for (address acc : holders) {
balances[acc] += balances[acc] * supplyDelta / totalSupply;
}
}
  • ERC777 token: Can re-enter into collectFee() if s_collector is a contract implementing tokensReceived(). Leads to reentrancy vulnerabilities.

function send(address recipient, uint256 amount, bytes memory data) external {
_callTokensToSend(...); // Can call reentering hooks
_move(msg.sender, recipient, amount);
_callTokensReceived(...); // Reentrancy entry point
}
  • Blacklisting token: Transfer fails if s_collector is blacklisted. Results in permanent locking of funds in the contract.

function transfer(address recipient, uint256 amount) external returns (bool) {
require(!blacklisted[msg.sender] && !blacklisted[recipient], "Blacklisted");
_transfer(msg.sender, recipient, amount);
return true;
}

Recommended Mitigation

- i_weth.transfer(s_collector, collection);
+ SafeERC20.safeTransfer(i_weth, s_collector, i_weth.balanceOf(address(this)));

Also consider implementing reentrancy guards around ETH and token transfers to further harden the contract against ERC777-based attacks.

Updates

Lead Judging Commences

yeahchibyke Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.