Summary
The claim
function is not updating any state. Thus, any subsequent claim will hold the same result as the first. If the user was allowed to claim the first time, he will still be allowed to claim later.
Vulnerability Details
The only checks performed by the claim
function are the msg.value
received and that the user is allowed to claim using the provided merkle tree proof.
There is no check to know if the user already claimed.
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
@> if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
@> if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
emit Claimed(account, amount);
@> i_airdropToken.safeTransfer(account, amount);
}
Impact
A malicious user could claim multiple times its allocation of the airdrop, until the contract is depleted or until it becomes uneconomical to claim (if the claimed value is less than the paid fee).
Tools Used
Manual review
Recommendations
Add a mapping of users that already claimed the airdrop and check that they are not present in the mapping before transferring the tokens.
uint256 private constant FEE = 1e9;
IERC20 private immutable i_airdropToken;
bytes32 private immutable i_merkleRoot;
+ mapping(address => bool) private userClaimed;
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
+ if(userClaimed[account]) revert;
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
+ userClaimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}