Root + Impact
Description
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
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();
}
}
function getMessageHash(address receiver) public view returns (bytes32) {
uint256 amount = i_snow.balanceOf(receiver);
return _hashTypedDataV4(
keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})))
);
}
Risk
Likelihood:
Impact:
Proof of Concept
function testRaceConditionExploit() public {
deal(address(weth), alice, 100 ether);
vm.startPrank(alice);
weth.approve(address(snow), 100 ether);
snow.buySnow(100);
vm.stopPrank();
merkle = new Merkle();
bytes32[] memory leaves = new bytes32[](2);
leaves[0] = keccak256(bytes.concat(keccak256(abi.encode(alice, 100))));
leaves[1] = keccak256(bytes.concat(keccak256(abi.encode(bob, 50))));
bytes32 merkleRoot = merkle.getRoot(leaves);
bytes32[] memory merkleProof = merkle.getProof(leaves, 0);
airdrop = new SnowmanAirdrop(merkleRoot, address(snow), address(snowman));
deal(address(weth), alice, 200 ether);
vm.startPrank(alice);
weth.approve(address(snow), 200 ether);
snow.buySnow(50);
vm.stopPrank();
vm.prank(alice);
snow.approve(address(airdrop), type(uint256).max);
bytes32 messageHash = airdrop.getMessageHash(alice);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, messageHash);
vm.expectRevert();
vm.prank(alice);
airdrop.claimSnowman(alice, merkleProof, v, r, s);
}
Recommended Mitigation
- remove this code
+ add this code
+ // Store the expected amount in merkle tree at contract creation
+ mapping(address => uint256) private s_airdropAmounts;
+ constructor(bytes32 _merkleRoot, address _snow, address _snowman, address[] memory _recipients, uint256[] memory _amounts) EIP712("Snowman Airdrop", "1") {
+ // Store expected amounts for each recipient
+ for (uint256 i = 0; i < _recipients.length; i++) {
+ s_airdropAmounts[_recipients[i]] = _amounts[i];
+ }
+ // ... rest of constructor
+ }
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
// ... validation checks ...
- uint256 amount = i_snow.balanceOf(receiver);
+ uint256 amount = s_airdropAmounts[receiver]; // Use fixed amount from merkle tree
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
// ... rest of function
}
function getMessageHash(address receiver) public view returns (bytes32) {
- uint256 amount = i_snow.balanceOf(receiver);
+ uint256 amount = s_airdropAmounts[receiver]; // Use fixed amount for signature
return _hashTypedDataV4(
keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})))
);
}