Snowman Merkle Airdrop

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

Snow tokens Locked in SnowmanAirdrop Contract

Author Revealed upon completion

Description

  • The SnowmanAirdrop contract is designed to allow users to stake their Snow tokens in exchange for Snowman NFTs. However, there is a critical issue where staked Snow tokens are permanently locked in the contract. The contract lacks any mechanism to either return these staked tokens to users or allow them to be withdrawn by the contract owner. This effectively means users lose their staked tokens forever, which is a significant deviation from the expected staking functionality.

function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
revert SA__InvalidSignature();
}
uint256 amount = i_snow.balanceOf(receiver);
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert SA__InvalidProof();
}
@> i_snow.safeTransferFrom(receiver, address(this), amount); // send tokens to contract... akin to burning
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}

Risk

Likelihood:

  • The issue occurs every time a user claims their Snowman NFT through the claimSnowman() function.

  • The contract has no mechanism to handle the staked tokens after the NFT minting.

Impact:

  • Users permanently lose their staked Snow tokens.

  • This defeats the purpose of a staking mechanism, which typically allows users to retrieve their staked tokens.

Proof of Concept

// Current problematic code in claimSnowman function
i_snow.safeTransferFrom(receiver, address(this), amount); // send tokens to contract... akin to burning
// The staked tokens are now permanently locked in the contract
// Users cannot:
// 1. Unstake their tokens
// 2. Retrieve their tokens after receiving the NFT
// 3. Use their staked tokens for any other purpose

Recommended Mitigation

// Add state variables to track staked tokens and NFT ownership
+ mapping(address => uint256) private s_stakedTokens;
+ mapping(address => bool) private s_hasUnstaked;
// Add events for staking and unstaking
+ event TokensUnstaked(address indexed user, uint256 amount);
// Modify the claimSnowman function to track staked tokens
- i_snow.safeTransferFrom(receiver, address(this), amount); // send tokens to contract... akin to burning
+ i_snow.safeTransferFrom(receiver, address(this), amount);
+ s_stakedTokens[receiver] = amount;
// Add unstakeTokens function
+ function unstakeTokens() external nonReentrant {
+ require(!s_hasUnstaked[msg.sender], "Already unstaked");
+ require(s_hasClaimedSnowman[msg.sender], "No Snowman NFT claimed");
+ uint256 stakedAmount = s_stakedTokens[msg.sender];
+ require(stakedAmount > 0, "No tokens staked");
+
+ s_hasUnstaked[msg.sender] = true;
+ s_stakedTokens[msg.sender] = 0;
+
// Burn the Snowman NFT (need to implement burnSnowman function in Snowman contract)
+ i_snowman.burnSnowman(msg.sender, nftAmount);
// Return staked tokens
+ i_snow.safeTransfer(msg.sender, stakedAmount);
+
+ emit TokensUnstaked(msg.sender, stakedAmount);
+ }
Updates

Lead Judging Commences

yeahchibyke Lead Judge
4 days ago
yeahchibyke Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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