Snowman Merkle Airdrop

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

The `collectFee()` function transfers all the WETH contract balance to the collector

The collectFee() function, transfers all the WETH balance of the contract, instead of just the fees, leading to drain all the contract's WETH

Description

When the collector calls 'collectFee()', they should receive the fees due to the 'buySnow()" function received when the users are buying snow.

But, it is actually taking the whole balance of the contrat, to transfer it to the collector, instead of determinating a separated amount between the fees and the actual balace of the contrat.

function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this)); // taking all the WETH as the collection
// instead of just the fees.
@> i_weth.transfer(s_collector, collection); // transfering all the WETH to the collector
(bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
}

Risk

Likelihood:

This occurs whenever the collector will call the collectFee() function.

Impact:

  • Which will result in a complete drain of the WETH balance of the contrat.




    Proof of Concept

To demonstrate this vulnerability, place the following test file into the test/TestSnow.t.sol file

function test_collectFee_drainsEverything() public {
// ── 1. Ashley buys 2 snow tokens in ETH
vm.prank(ashley);
snow.buySnow{value: 2 ether}(2);
console.log("Alice buys 2 Snow (ETH)");
console.log(" Contrat ETH balance :", address(snow).balance / 1e18, "ETH");
// ── 2. Victory buys 3 snow token in ETH
vm.prank(victory);
snow.buySnow{value: 3 ether}(3);
console.log("Bob buys 3 Snow (ETH)");
console.log(" Contrat ETH balance :", address(snow).balance / 1e18, "ETH");
// ── 3. Jerry buys 2 snow tokens in WETH
vm.prank(jerry);
snow.buySnow(2); // msg.value == 0 → WETH
console.log("Charlie buys 2 Snow (WETH)");
console.log(" Contrat WETH balance :", weth.balanceOf(address(snow)) / 1e18, "WETH");
// ── 4. Another user send ETH to this contract
// (Accident or donation)
vm.prank(makeAddr("stranger"));
vm.deal(makeAddr("stranger"), 1 ether);
(bool sent,) = address(snow).call{value: 1 ether}("");
assertTrue(sent);
console.log("Stranger sent 1 ETH directly");
// ── State before collectFee()
uint256 contractEthBefore = address(snow).balance;
uint256 contractWethBefore = weth.balanceOf(address(snow));
uint256 collectorEthBefore = address(collector).balance;
uint256 collectorWethBefore = weth.balanceOf(collector);
console.log("\n=== BEFORE collectFee ===");
console.log(" Contrat ETH :", contractEthBefore / 1e18, "ETH");
console.log(" Contrat WETH :", contractWethBefore / 1e18, "WETH");
console.log(" Collector ETH :", collectorEthBefore / 1e18, "ETH");
console.log(" Collector WETH:", collectorWethBefore / 1e18, "WETH");
// ── 5. Collector calls collectFee
vm.prank(collector);
snow.collectFee();
// ── State after collectFee()
uint256 contractEthAfter = address(snow).balance;
uint256 contractWethAfter = weth.balanceOf(address(snow));
uint256 collectorEthAfter = address(collector).balance;
uint256 collectorWethAfter = weth.balanceOf(collector);
console.log("\n=== AFTER collectFee ===");
console.log(" Contrat ETH :", contractEthAfter / 1e18, "ETH (attendu: 0)");
console.log(" Contrat WETH :", contractWethAfter / 1e18, "WETH (attendu: 0)");
console.log(" Collector ETH :", collectorEthAfter / 1e18, "ETH");
console.log(" Collector WETH:", collectorWethAfter / 1e18, "WETH");
// ── Assertions
// Contract is empty
assertEq(contractEthAfter, 0, "Contract should have 0 ETH");
assertEq(contractWethAfter, 0, "Contract should have 0 WETH");
// Collector received everything
assertEq(
collectorEthAfter,
collectorEthBefore + contractEthBefore,
"Collector a recu tout l ETH (incl. stranger)"
);
assertEq(
collectorWethAfter,
collectorWethBefore + contractWethBefore,
"Collector received all the WETH"
);
console.log("\n[POC] collectFee() has drained 100% of contract's funds.");
console.log("[POC] Stranger's ETH also got collected.");
}

Then run, forge test --mt test_collectFee_drainsEverything -vv


Recommended Mitigation

As a mitigation, create a specified amount of fees when collecting them, as exemple : feesCollected (variable) that get incremented whenever a fee is collected (ex: in buySnow).

Then, add/remove, the following pieces of codes :

function collectFee() external onlyCollector {
+ uint256 feesCollection = i_weth.feesCollected;
+ i_weth.transfer(s_collector, feesCollection);
- uint256 collection = i_weth.balanceOf(address(this));
- i_weth.transfer(s_collector, collection);
(bool collected,) = payable(s_collector).call{value: feesCollection}("");
require(collected, "Fee collection failed!!!");
}
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!