Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Incorrect Merkle root `s_merkleRoot` in `Deploy.s.sol`

Summary

An incorrect value is assigned to Deploy::s_merkleRoot.

Vulnerability Details

s_merkleRoot in Deploy.s.sol is supposed to contain the Merkle root generated by makeMerkle.js. However, makeMerkle.js specifies the claimable amount incorrectly:

@> const amount = (25 * 1e18).toString()

Given that the airdrop token is USDC, and the USDC token contract defines 6 decimals, the amount above esentially sets the claimable amount to 25 * 1e12 USDC, not to the intended 25 USDC. makeMerkle.js then encodes this incorrect amount value in the Merkle tree, resulting in an incorrect Merkle root - which in turn is used as an input parameter when deploying the MerkleAirdrop contract:

contract Deploy is Script {
address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
@> bytes32 public s_merkleRoot = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05;
// 4 users, 25 USDC each
uint256 public s_amountToAirdrop = 4 * (25 * 1e6);
// Deploy the airdropper
function run() public {
vm.startBroadcast();
@> MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC));
// Send USDC -> Merkle Air Dropper
IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop);
vm.stopBroadcast();
}

Impact

Due to the incorrect Merkle root used to deploy MerkleAirdrop, users are unable to claim the intended 25 USDC. Instead, the encoded data erroneously allows claims for 25 trillion USDC each—a claim that would fail as the contract does not and likely will never hold sufficient funds.

The following test demonstrates that users cannot claim 25 USDC (the trx will revert with MerkleAirdrop::MerkleAirdrop__InvalidProof), and neither they can claim 25 * 1e12 USDC, i.e. the airdrop amount that is encoded in the Merkle tree (the trx will revert with ERC20InsufficientBalance):

Proof of code
// import IERC20: import { MerkleAirdrop, IERC20 } from "../src/MerkleAirdrop.sol";
function testIncorrectMerkleRoot() public {
// Deploy script and test file use different setups.
// To demonstrate the vulnerability, we use the same setup in this test as in the deploy script.
// the test file had the correct Merkle root
// to demonstrate the bug, re-set it to the incorrect value used in Deploy.s.sol
bytes32 merkleRoot_bad = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05;
// the test file has the correct airdrop contract
// to demonstrate the bug, we need an airdrop contract the is deployed with the wrong root
// as done in Deploy.s.sol
MerkleAirdrop airdrop_bad = new MerkleAirdrop(merkleRoot_bad, token);
// the test file had the correct proof
// to demonstrate the bug, set it to the incorrect value that the incorrect makeMerkle.js generates
bytes32 proofOne_bad = 0x4fd31fee0e75780cd67704fbc43caee70fddcaa43631e2e1bc9fb233fada2394;
bytes32 proofTwo_bad = 0xc88d18957ad6849229355580c1bde5de3ae3b78024db2e6c2a9ad674f7b59f84;
bytes32[] memory proof_bad = new bytes32[](2);
proof_bad[0] = proofOne_bad;
proof_bad[1] = proofTwo_bad;
// setting up balances
vm.deal(collectorOne, airdrop_bad.getFee());
token.mint(address(airdrop_bad), 4 * 25 * 1e6);
assert(IERC20(token).balanceOf(address(airdrop_bad)) == 4 * 25 * 1e6);
// @note SCENARIO 1: collector tries to collect 25 USDC
// but the trx fails as a different value is encoded in the Merkle root
vm.prank(collectorOne);
// expectRevert would expect a revert for this call which is not we want, so
// moving this out from the airdrop.claim call
uint256 fee = airdrop_bad.getFee();
vm.expectRevert(MerkleAirdrop.MerkleAirdrop__InvalidProof.selector);
airdrop_bad.claim{ value: fee }(collectorOne, amountToCollect, proof_bad);
// add more USDC to the airdrop contract
//vm.deal(address(airdrop), 25*1e12)
// @note SCENARIO 2: collector tries to collect the value that is actually encoded in the Markle root
// but trx reverts because the airdrop contract does not have enough balance
//setting up balances
vm.deal(collectorOne, airdrop_bad.getFee());
uint256 amountEncodedInRoot = 25 * 1e18;
vm.prank(collectorOne);
vm.deal(address(airdrop_bad), amountEncodedInRoot);
// encode expected error
address expectedAddress = 0x6914631e3e71Bc75A1664e3BaEE140CC05cAE18B;
uint256 currentBalance = 100_000_000; // 1e8
uint256 requiredBalance = 25_000_000_000_000_000_000; // 25e18
// Encode the expected revert reason
// note need import: import { IERC20Errors } from "@openzeppelin/contracts//interfaces/draft-IERC6093.sol";
bytes memory encodedRevertReason = abi.encodeWithSelector(
IERC20Errors.ERC20InsufficientBalance.selector, expectedAddress, currentBalance, requiredBalance
);
vm.expectRevert(encodedRevertReason);
airdrop_bad.claim{ value: fee }(collectorOne, amountEncodedInRoot, proof_bad);
}

Eventually, the contract will have to be redeployed. Supposing that the address of the USDC contract is correct in Deploy.t.sol (which is actually not, as described in another finding), the 100 USDC sent to MerkleAirdrop will not be possible to recover.

Tools Used

Manual review, Foundry.

Recommendations

Perform the following corrective steps:

  • Correct the airdrop amount in makeMerkle.js:

const { StandardMerkleTree } = require("@openzeppelin/merkle-tree")
const fs = require("fs")
/*//////////////////////////////////////////////////////////////
INPUTS
//////////////////////////////////////////////////////////////*/
// @audit amount is incorrect, should be 25*1e6
- const amount = (25 * 1e18).toString()
+ const amount = (25 * 1e6).toString()
...
  • Built the Merkle tree again and replace the incorrect Merkle root in Deploy.s.sol with the correct one:

...
- bytes32 public s_merkleRoot = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05;
+ bytes32 public s_merkleRoot = 0x3b2e22da63ae414086bec9c9da6b685f790c6fab200c7918f2879f08793d77bd;
...
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

wrong-usdc-decimals-in-merkle

Support

FAQs

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