Platform: Code4rena
Start Date: 04/03/2024
Pot Size: $140,000 USDC
Total HM: 19
Participants: 69
Period: 21 days
Judge: 0xean
Total Solo HM: 4
Id: 343
League: ETH
Rank: 12/69
Findings: 2
Award: $2,037.77
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: MrPotatoMagic
Also found by: Aymen0909, alexfilippov314, pa6kuda, t4sk
2004.2337 USDC - $2,004.23
https://github.com/code-423n4/2024-03-taiko/blob/b6885955903c4ec6a0d72ebb79b124c6d0a1002b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol#L88-L94 https://github.com/code-423n4/2024-03-taiko/blob/b6885955903c4ec6a0d72ebb79b124c6d0a1002b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol#L40 https://github.com/code-423n4/2024-03-taiko/blob/b6885955903c4ec6a0d72ebb79b124c6d0a1002b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol#L117-L118
Users cannot claim full amount hence leaving dust locked in the contract.
Function withdraw
can only be called between the time starting at claimEnd
and before claimEnd + witndrawalWindow
.
100% of airdrop can be claimed at or after claimEnd + witndrawalWindow
.
So to claim 100% of the airdrop, user's transcation must be processed at timestamp equal to claimEnd + witndrawalWindow
. Otherwise unclaimable dust remains.
But it's merely impossible for users to submit transaction so that it will be executed at a precise time.
// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "forge-std/src/Test.sol"; import "forge-std/src/console2.sol"; // forge test --match-path test/poc/air-drop-dust.sol -vvv contract AirDrop { uint64 public claimEnd = uint64(block.timestamp + 2 days); uint64 public withdrawalWindow = 8 days; mapping(address addr => uint256 amountClaimed) public claimedAmount; mapping(address addr => uint256 amountWithdrawn) public withdrawnAmount; error WITHDRAWALS_NOT_ONGOING(); modifier ongoingWithdrawals() { if (claimEnd > block.timestamp || claimEnd + withdrawalWindow < block.timestamp) { revert WITHDRAWALS_NOT_ONGOING(); } _; } constructor() { claimedAmount[msg.sender] = 1e18; } function withdraw(address user) external ongoingWithdrawals { (, uint256 amount) = getBalance(user); withdrawnAmount[user] += amount; } function getBalance(address user) public view returns (uint256 balance, uint256 withdrawableAmount) { balance = claimedAmount[user]; // If balance is 0 then there is no balance and withdrawable amount if (balance == 0) return (0, 0); // Balance might be positive before end of claiming (claimEnd - if claimed already) but // withdrawable is 0. if (block.timestamp < claimEnd) return (balance, 0); // Hard cap timestamp - so range cannot go over - to get more allocation over time. uint256 timeBasedAllowance = balance * (min(block.timestamp, claimEnd + withdrawalWindow) - claimEnd) / withdrawalWindow; withdrawableAmount = timeBasedAllowance - withdrawnAmount[user]; } function min(uint256 a, uint256 b) internal pure returns (uint256) { return a <= b ? a : b; } } contract AirDropTest is Test { AirDrop private air_drop; uint256 private deadline; function setUp() public { air_drop = new AirDrop(); deadline = uint256(air_drop.claimEnd() + air_drop.withdrawalWindow()); } function test_dust() public { vm.warp(deadline - 1); air_drop.withdraw(address(this)); uint256 withdrawn = air_drop.withdrawnAmount(address(this)); console2.log("withdrawn", withdrawn); } function test_revert_after_deadline() public { vm.warp(deadline + 1); vm.expectRevert(); air_drop.withdraw(address(this)); // uint256 withdrawn = air_drop.withdrawnAmount(address(this)); // console2.log("withdrawn", withdrawn); } }
Command
forge test --match-path test/poc/air-drop-dust.sol -vvv
Output - claiming before expiry leaves dust. Claiming after expiry reverts.
[PASS] test_dust() (gas: 42605) Logs: withdrawn 999998553240740740 [PASS] test_revert_after_deadline() (gas: 13269) Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 670.06ยตs (241.28ยตs CPU time)
Foundry
Extend withdrawable period, for example by 10 days
modifier ongoingWithdrawals() { if (claimEnd > block.timestamp || claimEnd + withdrawalWindow + 10 days < block.timestamp) { revert WITHDRAWALS_NOT_ONGOING(); } _; }
Timing
#0 - c4-pre-sort
2024-03-29T09:43:54Z
minhquanym marked the issue as duplicate of #245
#1 - c4-judge
2024-04-10T11:17:00Z
0xean changed the severity to 3 (High Risk)
#2 - c4-judge
2024-04-10T11:17:38Z
0xean marked the issue as partial-75
#3 - c4-judge
2024-04-10T11:22:05Z
0xean marked the issue as satisfactory
๐ Selected for report: MrPotatoMagic
Also found by: 0x11singh99, DadeKuma, Fassi_Security, JCK, Kalyan-Singh, Masamune, Myd, Pechenite, Sathish9098, Shield, albahaca, alexfilippov314, cheatc0d3, clara, foxb868, grearlake, hihen, imare, joaovwfreire, josephdara, ladboy233, monrel, n1punp, oualidpro, pa6kuda, pfapostol, rjs, slvDev, sxima, t0x1c, t4sk, zabihullahazadzoi
33.5408 USDC - $33.54
TaikoL1
can be deployed on L2 as a base contract for L3. Hence when the reentrancy lock is loaded using transient storage, it should compare chain id of base chain, instead of chain id = 1.
Same for Bridge.sol
For example
if (block.chainid == BASE_CHAIN_ID) { assembly { tstore(_REENTRY_SLOT, _reentry) } }
This BASE_CHAIN_ID
can be initialized as an immutable variable.
Same signature can be used to claim grants on multiple chain, if the contract is deployed on multiple chains. It is recommend to hash chain id and address of the contract.
bytes32 hash = keccak256(abi.encodePacked("Withdraw unlocked Taiko token to: ", _to, block.chainid, address(this)));
Context
is packed into 2 slots (when used in storage) so the gap is off by one. Correct code is below
/// @dev Slots 3 and 4. Context private __ctx; /// @notice Mapping to store banned addresses. /// @dev Slot 5. mapping(address addr => bool banned) public addressBanned; /// @notice Mapping to store the proof receipt of a message from its hash. /// @dev Slot 6. mapping(bytes32 msgHash => ProofReceipt receipt) public proofReceipt; uint256[44] private __gap;
#0 - minhquanym
2024-03-30T16:32:19Z
L-2 is dup of #60 L-3 is dup of #15
#1 - c4-pre-sort
2024-03-30T16:32:23Z
minhquanym marked the issue as sufficient quality report
#2 - dantaik
2024-04-02T13:16:07Z
#3 - c4-sponsor
2024-04-02T13:16:23Z
dantaik (sponsor) confirmed
#4 - c4-judge
2024-04-10T10:49:27Z
0xean marked the issue as grade-b