Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

State Isolation Between Script Executions

Root + Impact

Description

Scripts should generate a Merkle tree based on actual token balances that will exist when the airdrop contract is deployed. Users should be able to claim tokens using valid Merkle proofs that match their real balances.

// GenerateInput.sol
function _createJSON() internal returns (string memory) {
helper = new Helper();
helper.run(); // @> Creates NEW temporary contract instances
snowAmountAlice = helper.aliceSB(); // @> Gets balances from temporary contracts
snowAmountBob = helper.bobSB();
// These balances are from contracts that won't exist in production
}

Risk

Likelihood:

  • Every script execution creates isolated blockchain state in Foundry

  • The deployment will always create different contract instances than the test instances

Impact:

  • Complete airdrop system failure - no user can claim tokens

  • Merkle proofs are valid for non-existent contract state

  • Production contract has zero connection to the generated Merkle tree

Proof of Concept

Deploy the airdrop through Helper which creates a Snow contract instance, then run GenerateInput which creates a different Snow instance - the generated Merkle tree with root 0xc0b6787abae0a5066bc2d09eaec944c58119dc18be796e93de5b2bf9f80ea79a

function testStateIsolationBug() public {
// Step 1: Run GenerateInput - creates temporary Snow contract
GenerateInput gen = new GenerateInput();
gen.run(); // Creates Helper which deploys Snow at address X, users earn tokens
// Step 2: Run SnowMerkle - generates Merkle tree from input.json
SnowMerkle merkle = new SnowMerkle();
merkle.run(); // Reads balances: Alice=1, Bob=1, etc. and creates root
// Step 3: Deploy actual airdrop - creates NEW Snow contract at different address
DeploySnowmanAirdrop deployer = new DeploySnowmanAirdrop();
(SnowmanAirdrop airdrop, Snow snow,) = deployer.deploySnowmanAirdrop();
// The Snow contract is different - users have 0 balance in the new instance
assertEq(snow.balanceOf(alice), 0); // Alice has 0, but Merkle expects 1
// Even with valid proof from output.json, claim will fail
bytes32[] memory proof = new bytes32[](3);
proof[0] = 0xf99782cec890699d4947528f9884acaca174602bb028a66d0870534acf241c52;
proof[1] = 0xbc5a8a0aad4a65155abf53bb707aa6d66b11b220ecb672f7832c05613dba82af;
proof[2] = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
vm.prank(alice);
airdrop.claim(alice, 1, proof); // Will revert - contract state mismatch
}

Recommended Mitigation

Pass the deployed Snow contract instance as a parameter to _createJSON() and read balances directly from that contract instead of creating new Helper instances with temporary contracts.

- remove this code
+ add this code
// In GenerateInput.sol
- function _createJSON() internal returns (string memory) {
- helper = new Helper();
- helper.run();
- snowAmountAlice = helper.aliceSB();
+ function _createJSON(Snow deployedSnow) internal view returns (string memory) {
+ // Read balances from the actual deployed Snow contract instance
+ snowAmountAlice = deployedSnow.balanceOf(alice);
+ snowAmountBob = deployedSnow.balanceOf(bob);
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 13 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!