high

Wrong Merkle Root used in `Deploy.s.sol` script causing eligible user cant claim

Selected Submission

Wrong Merkle Root used in Deploy.s.sol script causing eligible user cant claim

Severity

High Risk

Relevant GitHub Links

https://github.com/Cyfrin/2024-04-airdropper/blob/781cf225664a4ad11e9654aaa39cc528016bf214/script/Deploy.s.sol#L9

Summary

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:

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:

@>  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.

PoC

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:

-   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:

// 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:

Failing tests:
Encountered 1 failing test in test/MerkleAirdropDeployScriptTest.t.sol:MerkleAirdropTest
[FAIL. Reason: MerkleAirdrop__InvalidProof()] testPOCUsingDeployScriptContractBrokenBecauseWrongMerkleRoot() (gas: 32340)

Impact

user cannot claim the airdrop

Tools Used

manual review and foundry

Recommendations

Changing the correct Merkle Root can solve this problem.

Code

Using modified makeMerkle.js we can generate the correct value:

.
.
.
/*//////////////////////////////////////////////////////////////
                             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:

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:

-   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:

    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:

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)