AirDropper

AI First Flight #5
Beginner FriendlyDeFiFoundry
EXP
View results
Submission Details
Severity: high
Valid

Merkle root hard-coded in Deploy.s.sol commits 25e18 per recipient for 6-decimal USDC funded with 100e6, so no claim can pass both the proof check and the balance check and the airdrop can never pay out

Root + Impact

Description

  • Each recipient should be able to claim 25 USDC. The script's own comment and arithmetic (L10-L11) fund 4 × 25e6 = 100e6 base units of 6-decimal USDC, so each leaf in the deployed root should commit 25e6.

  • s_merkleRoot (L9) is the root of a tree in which every leaf commits 25e18, i.e. 25 × 1e18 in 18-decimal arithmetic. The PoC rebuilds that tree from the four README addresses and arrives at exactly this root, matching tree.json leaf for leaf. claim pays exactly the amount in the leaf, and the root is immutable. As a result, claim(user, 25e6, proof) fails the proof check, and claim(user, 25e18, proof) passes it but needs 2.5e19 base units against a balance of 1e8. The README's assumption that the root is correct holds only "so long as the inputs are correct" (L120). Here the input amount is wrong for the token, and its output is hard-coded in the in-scope Deploy.s.sol.

// script/Deploy.s.sol L9-L11
@> bytes32 public s_merkleRoot = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05; // L9: every leaf commits 25e18
// 4 users, 25 USDC each
@> uint256 public s_amountToAirdrop = 4 * (25 * 1e6); // L11: funding is 100e6 (6 decimals)
​
// src/MerkleAirdrop.sol L17 and L34-L39: the root is fixed, and claim() pays exactly the amount in the leaf
@> bytes32 private immutable i_merkleRoot; // L17
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount)))); // L34
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
emit Claimed(account, amount);
@> i_airdropToken.safeTransfer(account, amount); // L39

Risk

Likelihood:

  • The failure happens on every claim against the deployment the README says will be run. The root is a hard-coded literal, and no amount a user can submit passes both the proof check and the balance check.

  • The repository's own test uses a different root, 0x3b2e22da..., which is the correct 25e6 tree. The test suite therefore passes while the deployed configuration can never pay out, and nothing in the development flow surfaces the mismatch before deployment.

Impact:

  • The core function is unusable for all four recipients. A claim for 25e6 reverts with MerkleAirdrop__InvalidProof, and a claim for 25e18 reverts on insufficient balance. A 256-run fuzz over every amount in [0, 100e6], using each user's real proof, finds no amount that verifies.

  • The funded 100 USDC is locked: the root is immutable and the contract has no sweep or setter. In the PoC, the owner's claimFees and calls to common rescue/setter selectors leave the balance untouched. Paying even one committed claim would need 25,000,000,000,000 USDC. This bug is independent of the token-address mismatch reported separately: correcting the token address alone still leaves every claim reverting.

Proof of Concept

setUp mirrors Deploy.run(). It deploys MerkleAirdrop with the L9 root copied verbatim and a 6-decimal mock token, then funds it with the L11 amount (100e6). The token-address mismatch is assumed fixed so that this bug can be tested on its own. The tests:

  • rebuild both trees in Solidity: the 25e18 leaves give the deploy root and match tree.json, and the 25e6 leaves give the repository's test root;

  • show that the correct root pays out (baseline);

  • show that 25e6 reverts with InvalidProof, both with the 25e6 proof and with each user's real tree.json proof;

  • show that 25e18 passes verification and then reverts with ERC20InsufficientBalance, an error reachable only after MerkleProof.verify succeeds;

  • include a control showing the 25e18 claim succeeds once the contract holds 25e18;

  • fuzz every payable amount;

  • show that the owner cannot recover the balance.

The mock is OZ ERC20. On real zkSync USDC the balance revert carries that token's own message rather than the OZ custom error.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
​
import { Test, console2 } from "forge-std/Test.sol";
import { MerkleAirdrop, IERC20 } from "../src/MerkleAirdrop.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
​
/// 6-decimal stand-in for USDC (the token Deploy.s.sol funds with 4 * (25 * 1e6)).
contract MockUSDC6 is ERC20 {
constructor() ERC20("USD Coin", "USDC") { }
​
function decimals() public pure override returns (uint8) {
return 6;
}
​
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
​
contract PoC_AD_03 is Test {
// --- Literals copied verbatim from script/Deploy.s.sol ---
bytes32 constant DEPLOY_ROOT = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05; // L9
uint256 constant DEPLOY_FUNDING = 4 * (25 * 1e6); // L11 ("4 users, 25 USDC each")
​
// --- Root used by the repo's own test (test/MerkleAirdropTest.t.sol L12) ---
bytes32 constant TEST_ROOT = 0x3b2e22da63ae414086bec9c9da6b685f790c6fab200c7918f2879f08793d77bd;
​
uint256 constant INTENDED = 25 * 1e6; // 25 USDC at 6 decimals
uint256 constant COMMITTED = 25 * 1e18; // what tree.json / the deploy root actually commits
​
// The four recipients (tree.json "values", same order as makeMerkle.js)
address constant U1 = 0x20F41376c713072937eb02Be70ee1eD0D639966C; // treeIndex 6
address constant U2 = 0x277D26a45Add5775F21256159F089769892CEa5B; // treeIndex 3
address constant U3 = 0x0c8Ca207e27a1a8224D1b602bf856479b03319e7; // treeIndex 4
address constant U4 = 0xf6dBa02C01AF48Cf926579F77C9f874Ca640D91D; // treeIndex 5
​
// tree.json "tree" array (index 0 = root)
bytes32 constant T1 = 0xc88d18957ad6849229355580c1bde5de3ae3b78024db2e6c2a9ad674f7b59f84;
bytes32 constant T2 = 0x4886895f1e3f66692dcdc184b4d644b690c617a17ae16b8b42a69b5f408d3012;
bytes32 constant T3 = 0xe48eabad7bcfec7251063d2cc38d66b3a0819db5e6a7b1afe47da4a2e412e945;
bytes32 constant T4 = 0xa10b5ae53077397fbf8f4a7509073ea8141f0709d7d6f5ac6b77d1f94e3d2456;
bytes32 constant T5 = 0x4fd31fee0e75780cd67704fbc43caee70fddcaa43631e2e1bc9fb233fada2394;
bytes32 constant T6 = 0x1bdffa54a876a85308607ea73aa2d85639236dea08dd009aa2a6e82dc8df9dde;
​
MockUSDC6 usdc;
MerkleAirdrop airdrop; // deployed exactly as Deploy.s.sol would (DEPLOY_ROOT, 100e6 funding)
address deployer = makeAddr("deployer");
​
address[4] users;
​
function setUp() public {
users = [U1, U2, U3, U4];
usdc = new MockUSDC6();
​
// Mirror Deploy.run() with the token address mismatch (AD-02) assumed fixed, to isolate this bug.
vm.startPrank(deployer);
airdrop = new MerkleAirdrop(DEPLOY_ROOT, IERC20(address(usdc)));
vm.stopPrank();
usdc.mint(deployer, DEPLOY_FUNDING);
vm.prank(deployer);
usdc.transfer(address(airdrop), DEPLOY_FUNDING);
​
for (uint256 i; i < 4; i++) {
vm.deal(users[i], 10 ether);
}
}
​
// ---------------------------------------------------------------- helpers
function _leaf(address a, uint256 amt) internal pure returns (bytes32) {
// Same leaf construction as MerkleAirdrop.claim (src L34)
return keccak256(bytes.concat(keccak256(abi.encode(a, amt))));
}
​
function _hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) {
// Same sorted-pair hashing as OZ 5.0.2 MerkleProof._hashPair
return a < b ? keccak256(abi.encode(a, b)) : keccak256(abi.encode(b, a));
}
​
/// Proofs from tree.json (StandardMerkleTree layout: children of node i are 2i+1, 2i+2)
function _deployProof(address u) internal pure returns (bytes32[] memory p) {
p = new bytes32[](2);
if (u == U1) { p[0] = T5; p[1] = T1; } // leaf at 6: sibling 5, then node 1
else if (u == U4) { p[0] = T6; p[1] = T1; } // leaf at 5: sibling 6, then node 1
else if (u == U2) { p[0] = T4; p[1] = T2; } // leaf at 3: sibling 4, then node 2
else { p[0] = T3; p[1] = T2; } // U3, leaf at 4: sibling 3, then node 2
}
​
// ------------------------------------------------- 1. what the root commits
/// The hard-coded deploy root is exactly the tree over (user, 25e18); the 25e6 tree gives the test root.
function test_DeployRootCommits25e18_TestRootCommits25e6() public pure {
// 25e18 leaves reproduce tree.json and the Deploy.s.sol root
assertEq(_leaf(U1, COMMITTED), T6, "U1 leaf @25e18");
assertEq(_leaf(U2, COMMITTED), T3, "U2 leaf @25e18");
assertEq(_leaf(U3, COMMITTED), T4, "U3 leaf @25e18");
assertEq(_leaf(U4, COMMITTED), T5, "U4 leaf @25e18");
bytes32 n1 = _hashPair(T3, T4);
bytes32 n2 = _hashPair(T5, T6);
assertEq(n1, T1, "n1");
assertEq(n2, T2, "n2");
assertEq(_hashPair(n1, n2), DEPLOY_ROOT, "deploy root == tree over 25e18 amounts");
​
// 25e6 leaves (the amount the script funds) build the TEST root, not the deploy root
// pairing taken from the repo test's proof for U1: first sibling 0x32cee634... = leaf(U3, 25e6)
assertEq(_leaf(U3, INTENDED), 0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838, "U3 leaf @25e6");
bytes32 a1 = _hashPair(_leaf(U1, INTENDED), _leaf(U3, INTENDED));
bytes32 a2 = _hashPair(_leaf(U2, INTENDED), _leaf(U4, INTENDED));
assertEq(a2, 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c, "U2/U4 node @25e6");
bytes32 root6 = _hashPair(a1, a2);
assertEq(root6, TEST_ROOT, "25e6 tree == test root");
assertTrue(root6 != DEPLOY_ROOT, "deploy root does not commit 25e6");
​
console2.log("leaf(U1, 25e6) =");
console2.logBytes32(_leaf(U1, INTENDED));
console2.log("committed / intended per user :", COMMITTED / INTENDED);
console2.log("committed per user / funded :", COMMITTED / DEPLOY_FUNDING);
}
​
// ------------------------------------------------- 2. baseline (intended behaviour)
/// With the correct 25e6 root (the repo test's root) and the same 100e6 funding, the claim works.
function test_Baseline_CorrectRootPaysOut() public {
MerkleAirdrop good = new MerkleAirdrop(TEST_ROOT, IERC20(address(usdc)));
usdc.mint(address(good), DEPLOY_FUNDING);
​
bytes32[] memory p = new bytes32[](2); // test/MerkleAirdropTest.t.sol L18-20
p[0] = 0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838;
p[1] = 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c;
​
vm.prank(U1);
good.claim{ value: 1e9 }(U1, INTENDED, p);
assertEq(usdc.balanceOf(U1), INTENDED, "baseline: U1 received 25 USDC");
}
​
// ------------------------------------------------- 3. limb 1: intended amount fails the proof
function test_Limb1_Intended25e6_RevertsInvalidProof() public {
// with the 25e6-tree proof (from the repo test)
bytes32[] memory p6 = new bytes32[](2);
p6[0] = 0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838;
p6[1] = 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c;
vm.prank(U1);
vm.expectRevert(MerkleAirdrop.MerkleAirdrop__InvalidProof.selector);
airdrop.claim{ value: 1e9 }(U1, INTENDED, p6);
​
// and with each user's real tree.json proof
for (uint256 i; i < 4; i++) {
vm.prank(users[i]);
vm.expectRevert(MerkleAirdrop.MerkleAirdrop__InvalidProof.selector);
airdrop.claim{ value: 1e9 }(users[i], INTENDED, _deployProof(users[i]));
}
assertEq(usdc.balanceOf(address(airdrop)), DEPLOY_FUNDING, "nothing paid");
}
​
// ------------------------------------------------- 4. limb 2: committed amount verifies but is unpayable
function test_Limb2_Committed25e18_ProofPasses_ButInsufficientBalance() public {
for (uint256 i; i < 4; i++) {
// ERC20InsufficientBalance is only reachable AFTER MerkleProof.verify succeeded (src L35-39)
vm.prank(users[i]);
vm.expectRevert(
abi.encodeWithSelector(
IERC20Errors.ERC20InsufficientBalance.selector, address(airdrop), DEPLOY_FUNDING, COMMITTED
)
);
airdrop.claim{ value: 1e9 }(users[i], COMMITTED, _deployProof(users[i]));
assertEq(usdc.balanceOf(users[i]), 0, "user received nothing");
}
assertEq(usdc.balanceOf(address(airdrop)), DEPLOY_FUNDING, "100e6 still stuck in airdrop");
}
​
/// Control: the 25e18 proofs are genuinely valid - only the balance blocks them. Paying one
/// claim would need 25e18 base units = 25,000,000,000,000 USDC.
function test_Control_25e18ClaimSucceedsOnlyIfContractHoldsTrillions() public {
usdc.mint(address(airdrop), COMMITTED); // impossible in practice for real USDC
vm.prank(U1);
airdrop.claim{ value: 1e9 }(U1, COMMITTED, _deployProof(U1));
assertEq(usdc.balanceOf(U1), COMMITTED);
console2.log("USDC (whole units) needed for ONE claim:", COMMITTED / 1e6);
}
​
// ------------------------------------------------- 5. no payable amount verifies (fuzz)
/// For every amount the contract could actually pay (<= 100e6), the real proofs fail.
function testFuzz_NoPayableAmountVerifies(uint256 amount, uint8 who) public {
amount = bound(amount, 0, DEPLOY_FUNDING);
address u = users[who % 4];
vm.prank(u);
vm.expectRevert(MerkleAirdrop.MerkleAirdrop__InvalidProof.selector);
airdrop.claim{ value: 1e9 }(u, amount, _deployProof(u));
}
​
// ------------------------------------------------- 6. limb 4: funds locked, owner cannot recover
function test_Limb4_FundsLockedOwnerCannotRecover() public {
// every user tries both amounts; all fail
for (uint256 i; i < 4; i++) {
vm.startPrank(users[i]);
vm.expectRevert();
airdrop.claim{ value: 1e9 }(users[i], INTENDED, _deployProof(users[i]));
vm.expectRevert();
airdrop.claim{ value: 1e9 }(users[i], COMMITTED, _deployProof(users[i]));
vm.stopPrank();
}
​
// root is immutable and observable
assertEq(airdrop.getMerkleRoot(), DEPLOY_ROOT);
​
// the owner's only privileged function moves ETH, not the token
vm.prank(deployer);
airdrop.claimFees();
assertEq(usdc.balanceOf(address(airdrop)), DEPLOY_FUNDING, "claimFees does not touch USDC");
​
// no sweep / rescue / root setter exists: unknown selectors revert (no fallback/receive)
bytes[4] memory attempts = [
abi.encodeWithSignature("sweep(address)", deployer),
abi.encodeWithSignature("withdraw(address,uint256)", deployer, DEPLOY_FUNDING),
abi.encodeWithSignature("setMerkleRoot(bytes32)", TEST_ROOT),
abi.encodeWithSignature("rescueTokens(address,address,uint256)", address(usdc), deployer, DEPLOY_FUNDING)
];
for (uint256 i; i < attempts.length; i++) {
vm.prank(deployer);
(bool ok,) = address(airdrop).call(attempts[i]);
assertFalse(ok, "no recovery entry point");
}
assertEq(usdc.balanceOf(address(airdrop)), DEPLOY_FUNDING, "100 USDC permanently locked");
assertEq(usdc.balanceOf(deployer), 0);
console2.log("USDC locked in airdrop (base units):", usdc.balanceOf(address(airdrop)));
}
}

Run from the repository root, with the file saved as test/PoC_AD-03.t.sol:

FOUNDRY_FFI=false FOUNDRY_DISABLE_NIGHTLY_WARNING=1 forge test --match-path test/PoC_AD-03.t.sol -vv

Result: Suite result: ok. 7 passed; 0 failed; 0 skipped.

Recommended Mitigation

--- a/script/Deploy.s.sol
+++ b/script/Deploy.s.sol
@@ -6,7 +6,8 @@
contract Deploy is Script {
address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
- bytes32 public s_merkleRoot = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05;
+ // root of the tree over (address, 25 * 1e6) for the four README addresses
+ bytes32 public s_merkleRoot = 0x3b2e22da63ae414086bec9c9da6b685f790c6fab200c7918f2879f08793d77bd;
// 4 users, 25 USDC each
uint256 public s_amountToAirdrop = 4 * (25 * 1e6);
--- a/makeMerkle.js
+++ b/makeMerkle.js
@@ -4,7 +4,7 @@
/*//////////////////////////////////////////////////////////////
INPUTS
//////////////////////////////////////////////////////////////*/
-const amount = (25 * 1e18).toString()
+const amount = (25 * 1e6).toString()
const userToGetProofOf = "0x20F41376c713072937eb02Be70ee1eD0D639966C"
// (1)
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-03] Wrong Merkle Root used in `Deploy.s.sol` script causing eligible user cant claim

## Description `Deploy.s.sol` script provide the wrong value causing eligible address can not claim when the contract are deployed using the script. ## Vulnerability Details There are mismatch in value of merkle tree and merkle proof provided in `Deploy.s.sol` and `MerkleAirdropTest.t.sol` as the latter provided the correct value but the `Deploy.s.sol` script provide the wrong value causing eligible address can not claim when the contract are deployed using the script. NOTE: this assume the value of `s_zkSyncUSDC` used in `Deploy.s.sol` are already corrected using `0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4` otherwise it would revert and not showing `MerkleAirdrop__InvalidProof` error. If we run `yarn run makeMerkle` the following value would be returned: ```bash Merkle Root: 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05 Proof for address: 0x20F41376c713072937eb02Be70ee1eD0D639966C with amount: 25000000000000000000: [ '0x4fd31fee0e75780cd67704fbc43caee70fddcaa43631e2e1bc9fb233fada2394', '0xc88d18957ad6849229355580c1bde5de3ae3b78024db2e6c2a9ad674f7b59f84' ] ``` the Merkle Root value `0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05` are used in the `Deploy.s.sol` script: ```javascript @> bytes32 public s_merkleRoot = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05; // 4 users, 25 USDC each uint256 public s_amountToAirdrop = 4 * (25 * 1e6); ``` this is problematic because the intended value for every claim is `25 USDC` or `25 * 1e6` because USDC decimal is 6 instead of 18, and the above Merkle Root value are intended for `25 * 1e18`. the `claim` function cannot be called successfully because the contract only held `s_amountToAirdrop = 4 * (25 * 1e6)` USDC and user cannot claim `25 * 1e18` as this is wrong value. <details><summary>PoC</summary> Add your zksync rpc url `ZKSYNC_MAINNET_RPC_URL` to `.env` file. Add this helper code to `Deploy.s.sol` so our test can capture the address of `MerkleAirdrop` and `IERC20` contract: ```diff - function run() public { + function run() public returns (MerkleAirdrop, IERC20) { vm.startBroadcast(); MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC)); // Send USDC -> Merkle Air Dropper IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop); vm.stopBroadcast(); + // add helper + return (airdrop, IERC20(s_zkSyncUSDC)); } ``` Add `MerkleAirdropDeployScriptTest.t.sol` to `test` folder. `MerkleAirdropDeployScriptTest.t.sol`: ```javascript // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { MerkleAirdrop, IERC20 } from "../src/MerkleAirdrop.sol"; import { AirdropToken } from "./mocks/AirdropToken.sol"; import { Deploy } from "script/Deploy.s.sol"; import { Test } from "forge-std/Test.sol"; import { console } from "forge-std/Test.sol"; contract MerkleAirdropTest is Test, Deploy { MerkleAirdrop public airdrop; IERC20 public tokenIERC20; AirdropToken public token; uint256 amountToCollect = (25 * 1e6); // 25.000000 uint256 amountToSend = amountToCollect * 4; address collectorOne = 0x20F41376c713072937eb02Be70ee1eD0D639966C; // using proof from makeMerkle.js bytes32 proofOne = 0x4fd31fee0e75780cd67704fbc43caee70fddcaa43631e2e1bc9fb233fada2394; bytes32 proofTwo = 0xc88d18957ad6849229355580c1bde5de3ae3b78024db2e6c2a9ad674f7b59f84; bytes32[] public proof = [proofOne, proofTwo]; function setUp() public { // assume deployer have enough USDC balance, we use deal address correctUSDCAddress = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4; deal(address(IERC20(correctUSDCAddress)), address(msg.sender), amountToSend); console.log("USDC before:", IERC20(correctUSDCAddress).balanceOf(address(msg.sender))); // running the Deploy.s.sol script (airdrop, tokenIERC20) = Deploy.run(); console.log("USDC after:", IERC20(correctUSDCAddress).balanceOf(address(msg.sender))); } function testPOCUsingDeployScriptContractBrokenBecauseWrongMerkleRoot() public { // assume the correct USDC address are used in deploying MerkleAirdrop.sol uint256 startingBalance = tokenIERC20.balanceOf(collectorOne); vm.deal(collectorOne, airdrop.getFee()); vm.startPrank(collectorOne); airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof); vm.stopPrank(); uint256 endingBalance = tokenIERC20.balanceOf(collectorOne); assertEq(endingBalance - startingBalance, amountToCollect); } ``` after that run the following `forge test --zksync --fork-url $ZKSYNC_MAINNET_RPC_URL --mt testPOCUsingDeployScriptContractBrokenBecauseWrongMerkleRoot` the test will FAIL: ```bash Failing tests: Encountered 1 failing test in test/MerkleAirdropDeployScriptTest.t.sol:MerkleAirdropTest [FAIL. Reason: MerkleAirdrop__InvalidProof()] testPOCUsingDeployScriptContractBrokenBecauseWrongMerkleRoot() (gas: 32340) ``` </details> ## Impact user cannot claim the airdrop ## Recommendations Changing the correct Merkle Root can solve this problem. <details><summary>Code</summary> Using modified `makeMerkle.js` we can generate the correct value: ```diff . . . /*////////////////////////////////////////////////////////////// INPUTS //////////////////////////////////////////////////////////////*/ - const amount = (25 * 1e18).toString(); + const amount = (25 * 1e6).toString(); const userToGetProofOf = "0x20F41376c713072937eb02Be70ee1eD0D639966C"; // (1) const values = [ [userToGetProofOf, amount], ["0x277D26a45Add5775F21256159F089769892CEa5B", amount], ["0x0c8Ca207e27a1a8224D1b602bf856479b03319e7", amount], ["0xf6dBa02C01AF48Cf926579F77C9f874Ca640D91D", amount], ]; /*////////////////////////////////////////////////////////////// PROCESS //////////////////////////////////////////////////////////////*/ // (2) const tree = StandardMerkleTree.of(values, ["address", "uint256"]); // (3) console.log("Merkle Root:", tree.root); // (4) for (const [i, v] of tree.entries()) { // if (v[0] === userToGetProofOf) { // (3) const proof = tree.getProof(i); + console.log(`Proof for address: ${v} with amount: ${amount}:\n`, proof); - console.log(`Proof for address: ${userToGetProofOf} with amount: ${amount}:\n`, proof); - } } . . . ``` after that run `yarn run makeMerkle` and the following should returned: ```bash Merkle Root: 0x3b2e22da63ae414086bec9c9da6b685f790c6fab200c7918f2879f08793d77bd Proof for address: 0x20F41376c713072937eb02Be70ee1eD0D639966C,25000000 with amount: 25000000: [ '0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838', '0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c' ] Proof for address: 0x277D26a45Add5775F21256159F089769892CEa5B,25000000 with amount: 25000000: [ '0x2683f462a4457349d6d7ef62d4208ef42c89c2cff9543cd8292d9269d832c3e8', '0xdcad361f30c4a5b102a90b4ec310ffd75c577ccdff1c678adb20a6f02e923366' ] Proof for address: 0x0c8Ca207e27a1a8224D1b602bf856479b03319e7,25000000 with amount: 25000000: [ '0xee1cda884ead2c9f34338f48263e7edd6e5f35bf4f09c9c0930d995911004eed', '0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c' ] Proof for address: 0xf6dBa02C01AF48Cf926579F77C9f874Ca640D91D,25000000 with amount: 25000000: [ '0x1e6784ff835523401f4db6e3ab48fa5bdf523a46a5bc0410a5639d837352b194', '0xdcad361f30c4a5b102a90b4ec310ffd75c577ccdff1c678adb20a6f02e923366' ] ``` using the correct value above, we should change the `Deploy.s.sol` value of `s_merkleRoot`: `Deploy.s.sol`: ```diff - bytes32 public s_merkleRoot = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05; + bytes32 public s_merkleRoot = 0x3b2e22da63ae414086bec9c9da6b685f790c6fab200c7918f2879f08793d77bd; // 4 users, 25 USDC each uint256 public s_amountToAirdrop = 4 * (25 * 1e6); ``` after that add the following code to `MerkleAirdropDeployScriptTest.t.sol` that we already created beforehand. `MerkleAirdropDeployScriptTest.t.sol`: ```javascript function testUsingDeployScriptContractCorrectMerkleRoot() public { // assume the correct USDC address are used in deploying MerkleAirdrop.sol // using the correctly generated merkleProof bytes32[] memory correctProofs = new bytes32[](8); correctProofs[0] = 0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838; correctProofs[1] = 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c; correctProofs[2] = 0x2683f462a4457349d6d7ef62d4208ef42c89c2cff9543cd8292d9269d832c3e8; correctProofs[3] = 0xdcad361f30c4a5b102a90b4ec310ffd75c577ccdff1c678adb20a6f02e923366; correctProofs[4] = 0xee1cda884ead2c9f34338f48263e7edd6e5f35bf4f09c9c0930d995911004eed; correctProofs[5] = 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c; correctProofs[6] = 0x1e6784ff835523401f4db6e3ab48fa5bdf523a46a5bc0410a5639d837352b194; correctProofs[7] = 0xdcad361f30c4a5b102a90b4ec310ffd75c577ccdff1c678adb20a6f02e923366; // list of eligible addresses address[4] memory eligibleAddress = [ 0x20F41376c713072937eb02Be70ee1eD0D639966C, 0x277D26a45Add5775F21256159F089769892CEa5B, 0x0c8Ca207e27a1a8224D1b602bf856479b03319e7, 0xf6dBa02C01AF48Cf926579F77C9f874Ca640D91D ]; for (uint256 i = 0; i < eligibleAddress.length; ++i) { uint256 startingBalance = tokenIERC20.balanceOf(eligibleAddress[i]); vm.deal(eligibleAddress[i], airdrop.getFee()); // get corresponding proof for address i bytes32[] memory proofs = new bytes32[](2); proofs[0] = correctProofs[i * 2]; proofs[1] = correctProofs[i * 2 + 1]; vm.startPrank(eligibleAddress[i]); airdrop.claim{ value: airdrop.getFee() }(eligibleAddress[i], amountToCollect, proofs); vm.stopPrank(); uint256 endingBalance = tokenIERC20.balanceOf(eligibleAddress[i]); assertEq(endingBalance - startingBalance, amountToCollect); } } ``` then run the following command `forge test --zksync --fork-url $ZKSYNC_MAINNET_RPC_URL --mt testUsingDeployScriptContractCorrectMerkleRoot` the test result PASS: ```bash Ran 1 test for test/MerkleAirdropDeployScriptTest.t.sol:MerkleAirdropTest [PASS] testUsingDeployScriptContractCorrectMerkleRoot() (gas: 63544) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 76.19s (23.89s CPU time) ``` </details>

Support

FAQs

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

Give us feedback!