Snowman Merkle Airdrop

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

missing protection against reentrancy attack

Author Revealed upon completion

πŸ” Reentrancy Vulnerability in Snow.collectFee()

πŸ“‹ Description

πŸ”§ Normal Behavior

The collectFee() function allows the s_collector to collect all WETH and ETH held by the contract. It does this by calling the transfer() function on the WETH token and then sending the native ETH via a low-level call.

🐞 Specific Issue

The contract does not protect the collectFee() function against reentrancy attacks. Because it uses a low-level call to send ETH directly to the s_collector, a malicious contract can exploit this by reentering the collectFee() function (or other vulnerable external functions) during the fallback execution, draining ETH or disrupting the logic.

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 s_collector can be a contract, and nothing prevents it from being a malicious one.

  • As soon as collectFee() is called and ETH is transferred via call, the fallback function of the recipient contract can reenter the collectFee() function or another vulnerable function.

πŸ’₯ Impact

  • An attacker can recursively call collectFee() to drain ETH from the contract before the function finishes executing.

  • It may also interfere with any future logic or upgrade mechanisms that depend on internal state updates happening after the ETH transfer.


πŸ§ͺ Proof of Concept

Here’s a minimal malicious contract that can exploit the vulnerability if set as the s_collector:

contract MaliciousCollector {
address public vulnerable;
​
constructor(address _target) {
vulnerable = _target;
}
​
receive() external payable {
// Reenter collectFee while ETH is being sent
Snow(vulnerable).collectFee();
}
​
function attack() external {
Snow(vulnerable).collectFee(); // Initial call triggers reentrancy
}
}

βœ… Set s_collector = MaliciousCollector, then call attack() to recursively drain ETH.


πŸ› οΈ Recommended Mitigation

βœ… Use Reentrancy Protection

  1. Import OpenZeppelin’s ReentrancyGuard:

+ import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
  1. Apply nonReentrant to the vulnerable function:

- contract Snow is ERC20, Ownable {
+ contract Snow is ERC20, Ownable, ReentrancyGuard {
- function collectFee() external onlyCollector {
+ function collectFee() external onlyCollector nonReentrant {

🧱 Optional: Use Checks-Effects-Interactions Pattern

Move ETH transfer logic to the end of the function and update state before external interactions, if applicable.


πŸ›‘οΈ Security Recommendations

  • Always protect external ETH transfers with reentrancy guards.

  • Favor call with caution and ensure minimal logic is executed after external calls.

  • Restrict sensitive roles like s_collector to EOAs or audited contracts.


Support

FAQs

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