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

Emitting `MerkleAirdrop::Claimed` After Attempting to Transfer Tokens during a Claim Saves Gas in the Event of the Transfer Reverting

Summary

In the event that transferring tokens during a claim reverts, emitting the MerkleAirdrop::Claimed event raises gas costs for reverted transactions. Emitting the event before reverting incurs an extra 1,320 gas units. To mitigate this, emit the event after attempting to transfer tokens during a claim; no extra gas cost is incurred for successful calls.

Vulnerability Details

In the event that transferring tokens during a claim reverts, emitting the MerkleAirdrop::Claimed event raises gas costs for reverted transactions.

POC

MerkleAirdrop.sol

Temporarily add the following functions to the MerkleAirdrop contract.

import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
...
function emitClaimedBeforeRevert(address account, uint256 amount) external {
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}
function emitClaimedAfterRevert(address account, uint256 amount) external {
i_airdropToken.safeTransfer(account, amount);
// unreachable code, but present for context
emit Claimed(account, amount);
}

MerkleAirdropTest.t.sol

import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
...
function setUp() public {
...
// prevent the contract from receiving tokens, forcing transfers to revert
// token.transfer(address(airdrop), amountToSend);
...
}
...
function test_emitEventGasCost() public {
bytes memory ERC20Revert =
abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(airdrop), 0, 25e6);
vm.expectRevert(ERC20Revert);
airdrop.emitClaimedBeforeRevert(collectorOne, amountToCollect);
vm.expectRevert(ERC20Revert);
airdrop.emitClaimedAfterRevert(collectorOne, amountToCollect);
}

Run the Test

forge test --match-test test_emitEventGasCost -vvvv
Example Output
Ran 1 test for test/MerkleAirdropTest.t.sol:MerkleAirdropTest
[PASS] test_emitEventGasCost() (gas: 29674)
Traces:
[29674] MerkleAirdropTest::test_emitEventGasCost()
├─ [0] VM::expectRevert(ERC20InsufficientBalance(0x2946259E0334f33A064106302415aD3391BeD384, 0, 25000000 [2.5e7]))
│ └─ ← ()
├─ [8096] MerkleAirdrop::emitClaimedBeforeRevert(0x20F41376c713072937eb02Be70ee1eD0D639966C, 25000000 [2.5e7])
│ ├─ emit Claimed(account: 0x20F41376c713072937eb02Be70ee1eD0D639966C, amount: 25000000 [2.5e7])
│ ├─ [2875] AirdropToken::transfer(0x20F41376c713072937eb02Be70ee1eD0D639966C, 25000000 [2.5e7])
│ │ └─ ← ERC20InsufficientBalance(0x2946259E0334f33A064106302415aD3391BeD384, 0, 25000000 [2.5e7])
│ └─ ← ERC20InsufficientBalance(0x2946259E0334f33A064106302415aD3391BeD384, 0, 25000000 [2.5e7])
├─ [0] VM::expectRevert(ERC20InsufficientBalance(0x2946259E0334f33A064106302415aD3391BeD384, 0, 25000000 [2.5e7]))
│ └─ ← ()
├─ [6776] MerkleAirdrop::emitClaimedAfterRevert(0x20F41376c713072937eb02Be70ee1eD0D639966C, 25000000 [2.5e7])
│ ├─ [2875] AirdropToken::transfer(0x20F41376c713072937eb02Be70ee1eD0D639966C, 25000000 [2.5e7])
│ │ └─ ← ERC20InsufficientBalance(0x2946259E0334f33A064106302415aD3391BeD384, 0, 25000000 [2.5e7])
│ └─ ← ERC20InsufficientBalance(0x2946259E0334f33A064106302415aD3391BeD384, 0, 25000000 [2.5e7])
└─ ← ()
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.73ms (529.50µs CPU time)
Ran 1 test suite in 11.50ms (2.73ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

  • Gas cost to emit the event, then revert: 8,096

  • Gas cost to revert before emitting: 6,776

  • Savings in the event of a revert: 1,320

Impact

Emitting the event before reverting incurs an extra 1,320 gas units.

Tools Used

Manual Analysis, Foundry Tests

Recommendations

Emit the event after attempting to transfer tokens during a claim; no extra gas cost is incurred for successful calls.

function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
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);
+ emit Claimed(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.