Impact: H
Likelihood: H
When a claimer calls MerkleAirdrop.sol::claim() function, the merkleProofs for his address and 25e6 tokens amount should be provided in order to check whether the claimer is eligible to receive the airdropped tokens. The merkle root is used for this reason, but it is calculated for the wrong amount of tokens, literally for 25e18. This means that the merkle proof will never be verified for 25e6 claimed amount and the claimer will never receive her tokens.
It is less likely, but possible if an attacker knows about this issue, he will be able to receive 25e18 if he provides this amount during claim and if the MerkleAirdrop.sol balance of tokens is more than 25e18. This scenario may not be valid because the team intended to airdrop only 100e6 which is not enough to claim 25e18.
Likelihood:
The issue occurs every time a claimer claims her tokens because the amount of tokens used for merkle root calculation (25e18) is not the same as intended to airdrop (25e6).
Impact:
Claimer will not be able to claim their 25e6 amount of airdropped token.
If an attacker knows about an exact amount of tokens used for merkle root calculation and the contract MerkleAirdrop.sol is funded with amount more than that amount, the one person may claim 25e18 which is more than it was intended to airdrop by the protocol.
Run the provided script makeMerkle.js to calculate a merkle root for address 0x20F41376c713072937eb02Be70ee1eD0D639966C using the command npm run makeMerkle. The output shows that the amount is used 25e18 instead of 25e6:
Consider to add the following fork test to MerkleAirdropTest.t.sol and run it on fork of Zksync Era Mainnet using a command forge test --mt test_wrongMerkleRoot --fork-url $ZK_SYNC_MAINNET_URL --zksync -vvvvv . The test checks two cases:
The merkle proof is not verified when 25e6 is claimed.
The merkle proof is verified and 25e18 is sent to a claimer if he claimes 25e18 tokens and the MerkleAirdrop.sol is funded with this much amount of tokens - this scenario is less likely because the protocol is designed to airdrop only 100 USDC.
The Deploy.s.sol script should be fixed before running the test above because it has issues - use the correct USDC token address and remove cheatcodes and add return value to run() function:
Recalculate the merkle root and proofs using the correct amount of tokens in makeMerkle.js:
## 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>
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.