SnowmanAirdrop.sol::claimSnowman()
and External CallsThe claimSnowman()
function in SnowmanAirdrop.sol
performs multiple external calls: i_snow.balanceOf(receiver)
(external read), i_snow.safeTransferFrom(receiver, address(this), amount)
(external write), and i_snowman.mintSnowman(receiver, amount)
(external write). While the nonReentrant
modifier is present, the primary concern here is not a classic reentrancy attack (where a malicious contract re-enters before the first call finishes). Instead, the vulnerability arises from the sequence and reliance on mutable external state for critical verification steps, particularly when combined with the issues highlighted in [H-6] (unsafe signer) and [H-9] (Merkle proof bound to mutable balance).
This combination of vulnerabilities can lead to sophisticated replay attacks or double claims. If a user's token balance (i_snow.balanceOf(receiver)
) can change between the time a signed claim payload is generated off-chain (which might rely on a specific amount
snapshot) and the time the claimSnowman
function is executed on-chain, an attacker could potentially exploit this. A user might sign a message based on their balance, transfer tokens out, and then a malicious actor (or even the user themselves) could try to use the same signature/Merkle proof from a different wallet or context. If the amount
check is not strictly tied to the initial proof, this could lead to inconsistent state or unintended mints.
The vulnerable sequence in SnowmanAirdrop.sol::claimSnowman()
:
The proof of concept describes a scenario where "User signs a valid claim with balance of 100. Sends tokens to another wallet, who uses the same v,r,s
with valid Merkle proof. New wallet successfully mints due to balance check not tied to proof." This highlights the broader issue of state dependency and signature reusability.
Bind Merkle Proof Strictly to a Static amount
: The amount
used to generate the Merkle proof should be a fixed value from a snapshot (off-chain) and should be explicitly passed as an argument to claimSnowman
. The on-chain i_snow.balanceOf(receiver)
should not be used to derive this amount
for Merkle verification. This ensures that the Merkle proof verifies a specific (receiver, amount)
pair from a predefined eligibility list.
Implement EIP-712 Nonce-Based Replay Protection & msg.sender
Binding: This is paramount. The signature used for the claim must be unique to prevent replay. Including a unique nonce (managed by the contract) and binding the signature to msg.sender
(the caller of claimSnowman
) ensures that each claim can only be executed once by the rightful claimant. (Refer to the detailed mitigation in [H-6]).
Strict Checks-Effects-Interactions Ordering: While nonReentrant
helps, always ensure that all internal state changes (s_hasClaimedSnowman[receiver] = true;
) occur before any external calls are made. The current code has s_hasClaimedSnowman[receiver] = true;
after safeTransferFrom
but before mintSnowman
, which is acceptable, but ideally all internal state updates would precede any external interactions. The primary vulnerability here is in the data integrity for proof generation and signature validity, rather than a classic reentrancy bug.
(Note: The code diff for this mitigation largely overlaps with the proposed solutions for [H-6] and [H-9] as they are deeply interconnected.)
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.