Balance-Based Claim Manipulation
Summary
The airdrop claim amount is determined by the user's current Snow token balance rather than a fixed amount encoded in the Merkle tree. This design enables sophisticated flash loan attacks to claim disproportionate NFT amounts.
Vulnerability Details
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) {
uint256 amount = i_snow.balanceOf(receiver);
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
i_snowman.mintSnowman(receiver, amount);
}
Impact
Attackers can claim NFTs worth far more than their actual investment.
Proof of Concept
contract FlashLoanExploit {
function executeAttack() external {
uint256 loanAmount = 10_000e18;
flashLoanProvider.flashLoan(loanAmount);
}
function onFlashLoan(uint256 amount) external {
airdrop.claimSnowman(address(this), proof, v, r, s);
}
}
Recommendations
Option 1: Fixed Amount in Merkle Tree
struct ClaimData {
address receiver;
uint256 fixedAmount;
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, claimAmount))));
require(MerkleProof.verify(merkleProof, i_merkleRoot, leaf), "Invalid proof");
i_snowman.mintSnowman(receiver, claimAmount);
Option 2: Minimum Holding Period
mapping(address => uint256) public firstSnowAcquisition;
modifier requiresHoldingPeriod(address user) {
require(
block.timestamp >= firstSnowAcquisition[user] + MINIMUM_HOLDING_PERIOD,
"Insufficient holding period"
);
_;
}
Option 3: Snapshot-Based Claims
mapping(address => uint256) public snapshotBalances;
uint256 public snapshotBlock;
function claimSnowman(address receiver, uint256 snapshotAmount, bytes32[] calldata merkleProof) external {
require(snapshotBalances[receiver] == snapshotAmount, "Snapshot mismatch");
}