Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: low
Invalid

Checking `merkleProof` Calldata Length Is Not Zero Saves Gas for Proofs That Cannot Be Correct

Summary

Not checking the length of the provided merkleProof calldata for known bad proofs (zero length) and reverting early increases the gas cost for transactions that are guaranteed to revert by 567 gas units, while only increasing the gas cost for valid proofs by 27 gas units.

Vulnerability Details

Not checking the length of the provided merkleProof calldata for known bad proofs (zero length) and reverting early still makes the contract calculate a leaf and attempt to verify the proof.

POC

MerkleAirdrop.sol

Temporarily add the following functions to the MerkleAirdrop contract.

function merkleWithCheck(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (merkleProof.length == 0) {
revert MerkleAirdrop__InvalidProof();
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
}
function merkleWithoutCheck(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
}

MerkleAirdropTest.t.sol

function test_gasCostOfMerkleVerifyWithoutCheckEmpty() public {
bytes32[] memory emptyProof;
vm.startPrank(collectorOne);
vm.expectRevert(MerkleAirdrop.MerkleAirdrop__InvalidProof.selector);
airdrop.merkleWithoutCheck(collectorOne, amountToCollect, emptyProof);
vm.stopPrank();
}
function test_gasCostOfMerkleVerifyWithoutCheckValid() public {
vm.startPrank(collectorOne);
airdrop.merkleWithoutCheck(collectorOne, amountToCollect, proof);
vm.stopPrank();
}
function test_gasCostOfMerkleVerifyWithCheckEmpty() public {
bytes32[] memory emptyProof;
vm.startPrank(collectorOne);
vm.expectRevert(MerkleAirdrop.MerkleAirdrop__InvalidProof.selector);
airdrop.merkleWithCheck(collectorOne, amountToCollect, emptyProof);
vm.stopPrank();
}
function test_gasCostOfMerkleVerifyWithCheckValid() public {
vm.startPrank(collectorOne);
airdrop.merkleWithCheck(collectorOne, amountToCollect, proof);
vm.stopPrank();
}

Run the Tests

forge test --match-test "test_gasCostOfMerkleVerifyWith*" -vvvv
Example Output

Compare gas costs of each MerkleAirdrop::merkleWithCheck and MerkleAirdrop::merkleWithoutCheck call.

Ran 4 tests for test/MerkleAirdropTest.t.sol:MerkleAirdropTest
[PASS] test_gasCostOfMerkleVerifyWithCheckEmpty() (gas: 14060)
Traces:
[14060] MerkleAirdropTest::test_gasCostOfMerkleVerifyWithCheckEmpty()
├─ [0] VM::startPrank(0x20F41376c713072937eb02Be70ee1eD0D639966C)
│ └─ ← ()
├─ [0] VM::expectRevert(MerkleAirdrop__InvalidProof())
│ └─ ← ()
├─ [600] MerkleAirdrop::merkleWithCheck(0x20F41376c713072937eb02Be70ee1eD0D639966C, 25000000 [2.5e7], [])
│ └─ ← MerkleAirdrop__InvalidProof()
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
[PASS] test_gasCostOfMerkleVerifyWithCheckValid() (gas: 21147)
Traces:
[21147] MerkleAirdropTest::test_gasCostOfMerkleVerifyWithCheckValid()
├─ [0] VM::startPrank(0x20F41376c713072937eb02Be70ee1eD0D639966C)
│ └─ ← ()
├─ [1683] MerkleAirdrop::merkleWithCheck(0x20F41376c713072937eb02Be70ee1eD0D639966C, 25000000 [2.5e7], [0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838, 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c])
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
[PASS] test_gasCostOfMerkleVerifyWithoutCheckEmpty() (gas: 14517)
Traces:
[14517] MerkleAirdropTest::test_gasCostOfMerkleVerifyWithoutCheckEmpty()
├─ [0] VM::startPrank(0x20F41376c713072937eb02Be70ee1eD0D639966C)
│ └─ ← ()
├─ [0] VM::expectRevert(MerkleAirdrop__InvalidProof())
│ └─ ← ()
├─ [1167] MerkleAirdrop::merkleWithoutCheck(0x20F41376c713072937eb02Be70ee1eD0D639966C, 25000000 [2.5e7], [])
│ └─ ← MerkleAirdrop__InvalidProof()
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
[PASS] test_gasCostOfMerkleVerifyWithoutCheckValid() (gas: 21165)
Traces:
[21165] MerkleAirdropTest::test_gasCostOfMerkleVerifyWithoutCheckValid()
├─ [0] VM::startPrank(0x20F41376c713072937eb02Be70ee1eD0D639966C)
│ └─ ← ()
├─ [1656] MerkleAirdrop::merkleWithoutCheck(0x20F41376c713072937eb02Be70ee1eD0D639966C, 25000000 [2.5e7], [0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838, 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c])
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 7.98ms (18.19ms CPU time)
Ran 1 test suite in 12.07ms (7.98ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)

Impact

Not reverting early for proofs with a length of zero increase the gas cost for transactions that are guaranteed to revert.

Proof is Empty? Checks for Length? Gas Cost Net Loss/Gain
Yes No 1167
Yes Yes 600 567
No No 1656
No Yes 1683 -27

Implementing this check saves 567 gas units when an empty proof is provided, and only marginally increases gas prices for valid proofs by 27 units.

Tools Used

Manual Analysis, Foundry Tests

Recommendations

Check the length of provided merkleProof calldata length in MerkleAirdrop::claim is not zero. If it is zero, revert early with MerkleAirdrop::MerkleAirdrop__InvalidProof.

function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
+ if (merkleProof.length == 0) {
+ revert MerkleAirdrop__InvalidProof();
+ }
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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