diff --git a/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol b/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol index 488eb80e4a..5e0e655402 100644 --- a/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol +++ b/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol @@ -15,6 +15,7 @@ pragma solidity 0.8.24; import "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "lib/openzeppelin-contracts/contracts/governance/utils/IVotes.sol"; import "./MerkleClaimable.sol"; /// @title ERC20Airdrop @@ -25,8 +26,8 @@ contract ERC20Airdrop is MerkleClaimable { uint256[48] private __gap; function init( - uint64 _claimStarts, - uint64 _claimEnds, + uint64 _claimStart, + uint64 _claimEnd, bytes32 _merkleRoot, address _token, address _vault @@ -35,14 +36,33 @@ contract ERC20Airdrop is MerkleClaimable { initializer { __Essential_init(); - _setConfig(_claimStarts, _claimEnds, _merkleRoot); + __MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot); token = _token; vault = _vault; } - function _claimWithData(bytes calldata data) internal override { - (address user, uint256 amount) = abi.decode(data, (address, uint256)); + function claimAndDelegate( + address user, + uint256 amount, + bytes32[] calldata proof, + bytes calldata delegationData + ) + external + nonReentrant + { + // Check if this can be claimed + _verifyClaim(abi.encode(user, amount), proof); + + // Transfer the tokens IERC20(token).transferFrom(vault, user, amount); + + // Delegate the voting power to delegatee. + // Note that the signature (v,r,s) may not correspond to the user address, + // but since the data is provided by Taiko backend, it's not an issue even if + // client can change the data to call delegateBySig for another user. + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) = + abi.decode(delegationData, (address, uint256, uint256, uint8, bytes32, bytes32)); + IVotes(token).delegateBySig(delegatee, nonce, expiry, v, r, s); } } diff --git a/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol index 788c786fa8..ff1139765a 100644 --- a/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol +++ b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol @@ -47,8 +47,8 @@ contract ERC20Airdrop2 is MerkleClaimable { } function init( - uint64 _claimStarts, - uint64 _claimEnds, + uint64 _claimStart, + uint64 _claimEnd, bytes32 _merkleRoot, address _token, address _vault, @@ -58,14 +58,21 @@ contract ERC20Airdrop2 is MerkleClaimable { initializer { __Essential_init(); - // Unix timestamp=_claimEnds+1 marks the first timestamp the users are able to withdraw. - _setConfig(_claimStarts, _claimEnds, _merkleRoot); + __MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot); token = _token; vault = _vault; withdrawalWindow = _withdrawalWindow; } + function claim(address user, uint256 amount, bytes32[] calldata proof) external nonReentrant { + // Check if this can be claimed + _verifyClaim(abi.encode(user, amount), proof); + + // Assign the tokens + claimedAmount[user] += amount; + } + /// @notice External withdraw function /// @param user User address function withdraw(address user) external ongoingWithdrawals { @@ -102,9 +109,4 @@ contract ERC20Airdrop2 is MerkleClaimable { withdrawableAmount = timeBasedAllowance - withdrawnAmount[user]; } - - function _claimWithData(bytes calldata data) internal override { - (address user, uint256 amount) = abi.decode(data, (address, uint256)); - claimedAmount[user] += amount; - } } diff --git a/packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol b/packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol index 8bb7201af1..a20515f184 100644 --- a/packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol +++ b/packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol @@ -24,8 +24,8 @@ contract ERC721Airdrop is MerkleClaimable { uint256[48] private __gap; function init( - uint64 _claimStarts, - uint64 _claimEnds, + uint64 _claimStart, + uint64 _claimEnd, bytes32 _merkleRoot, address _token, address _vault @@ -34,15 +34,24 @@ contract ERC721Airdrop is MerkleClaimable { initializer { __Essential_init(); - _setConfig(_claimStarts, _claimEnds, _merkleRoot); + __MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot); token = _token; vault = _vault; } - function _claimWithData(bytes calldata data) internal override { - (address user, uint256[] memory tokenIds) = abi.decode(data, (address, uint256[])); + function claim( + address user, + uint256[] calldata tokenIds, + bytes32[] calldata proof + ) + external + nonReentrant + { + // Check if this can be claimed + _verifyClaim(abi.encode(user, tokenIds), proof); + // Transfer the tokens for (uint256 i; i < tokenIds.length; ++i) { IERC721Upgradeable(token).safeTransferFrom(vault, user, tokenIds[i]); } diff --git a/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol b/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol index 58e257687b..1019023c96 100644 --- a/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol +++ b/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol @@ -14,8 +14,7 @@ pragma solidity 0.8.24; -import { MerkleProofUpgradeable } from - "lib/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/MerkleProofUpgradeable.sol"; +import "lib/openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; import "../../common/EssentialContract.sol"; /// @title MerkleClaimable @@ -42,27 +41,6 @@ abstract contract MerkleClaimable is EssentialContract { _; } - function claim( - bytes calldata data, - bytes32[] calldata proof - ) - external - nonReentrant - ongoingClaim - { - bytes32 hash = keccak256(abi.encode("CLAIM_TAIKO_AIRDROP", data)); - - if (isClaimed[hash]) revert CLAIMED_ALREADY(); - - if (!MerkleProofUpgradeable.verify(proof, merkleRoot, hash)) { - revert INVALID_PROOF(); - } - - isClaimed[hash] = true; - _claimWithData(data); - emit Claimed(hash); - } - /// @notice Set config parameters /// @param _claimStart Unix timestamp for claim start /// @param _claimEnd Unix timestamp for claim end @@ -78,12 +56,43 @@ abstract contract MerkleClaimable is EssentialContract { _setConfig(_claimStart, _claimEnd, _merkleRoot); } - function _setConfig(uint64 _claimStart, uint64 _claimEnd, bytes32 _merkleRoot) internal { + // solhint-disable-next-line func-name-mixedcase + function __MerkleClaimable_init( + uint64 _claimStart, + uint64 _claimEnd, + bytes32 _merkleRoot + ) + internal + { + _setConfig(_claimStart, _claimEnd, _merkleRoot); + } + + function _verifyClaim(bytes memory data, bytes32[] calldata proof) internal ongoingClaim { + bytes32 hash = keccak256(abi.encode("CLAIM_TAIKO_AIRDROP", data)); + + if (isClaimed[hash]) revert CLAIMED_ALREADY(); + if (!_verifyMerkleProof(proof, merkleRoot, hash)) revert INVALID_PROOF(); + + isClaimed[hash] = true; + emit Claimed(hash); + } + + function _verifyMerkleProof( + bytes32[] calldata _proof, + bytes32 _merkleRoot, + bytes32 _value + ) + internal + pure + virtual + returns (bool) + { + return MerkleProof.verify(_proof, _merkleRoot, _value); + } + + function _setConfig(uint64 _claimStart, uint64 _claimEnd, bytes32 _merkleRoot) private { claimStart = _claimStart; claimEnd = _claimEnd; merkleRoot = _merkleRoot; } - - /// @dev Must revert in case of errors. - function _claimWithData(bytes calldata data) internal virtual; } diff --git a/packages/protocol/test/team/airdrop/ERC20Airdrop.t.sol b/packages/protocol/test/team/airdrop/ERC20Airdrop.t.sol new file mode 100644 index 0000000000..fedb847b8f --- /dev/null +++ b/packages/protocol/test/team/airdrop/ERC20Airdrop.t.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../../TaikoTest.sol"; + +contract MockERC20Airdrop is ERC20Airdrop { + function _verifyMerkleProof( + bytes32[] calldata, /*proof*/ + bytes32, /*merkleRoot*/ + bytes32 /*value*/ + ) + internal + pure + override + returns (bool) + { + return true; + } +} + +contract TestERC20Airdrop is TaikoTest { + address public owner = randAddress(); + + // Private Key: 0x1dc880d28041a41132437eae90c9e09c3b9e13438c2d0f6207804ceece623395 + address public Lily = 0x3447b15c1b0a27D339C812b98881eC64051068b3; + + bytes32 public constant merkleRoot = bytes32(uint256(1)); + bytes32[] public merkleProof; + uint64 public claimStart; + uint64 public claimEnd; + + TaikoToken token; + ERC20Airdrop airdrop; + + function setUp() public { + claimStart = uint64(block.timestamp + 10); + claimEnd = uint64(block.timestamp + 10_000); + merkleProof = new bytes32[](3); + + token = TaikoToken( deployProxy({ + name: "taiko_token", + impl: address(new TaikoToken()), + data: abi.encodeCall(TaikoToken.init, ("Taiko Token", "TKO", owner)) })); + + + airdrop = ERC20Airdrop( + deployProxy({ + name: "MockERC20Airdrop", + impl: address(new MockERC20Airdrop()), + data: abi.encodeCall( + ERC20Airdrop.init, (claimStart, claimEnd, merkleRoot, address(token), owner) + ) + }) + ); + + vm.roll(block.number + 1); + } + + function test_claimAndDelegate_with_wrong_delegation_data() public { + vm.warp(claimStart); + + bytes memory delegation = bytes(""); + + vm.expectRevert("ERC20: insufficient allowance"); // no allowance + vm.prank(Lily, Lily); + airdrop.claimAndDelegate(Lily, 100, merkleProof, delegation); + + vm.prank(owner, owner); + token.approve(address(airdrop), 1_000_000_000e18); + + vm.expectRevert(); // cannot decode the delegation data + vm.prank(Lily, Lily); + airdrop.claimAndDelegate(Lily, 100, merkleProof, delegation); + + address delegatee = randAddress(); + uint256 nonce = 1; + uint256 expiry = block.timestamp + 10_000; + uint8 v; + bytes32 r; + bytes32 s; + + delegation = abi.encode(delegatee, nonce, expiry, v, r, s); + + vm.expectRevert(); // signature invalid + vm.prank(Lily, Lily); + airdrop.claimAndDelegate(Lily, 100, merkleProof, delegation); + + // TODO(daniel): add a new test by initializing the right value for the above 6 variables. + } +} diff --git a/packages/protocol/test/team/airdrop/ERC20Airdrop2.t.sol b/packages/protocol/test/team/airdrop/ERC20Airdrop2.t.sol new file mode 100644 index 0000000000..fc0c0a744e --- /dev/null +++ b/packages/protocol/test/team/airdrop/ERC20Airdrop2.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../../TaikoTest.sol"; + +contract MyERC20 is ERC20 { + constructor(address owner) ERC20("Taiko Token", "TKO") { + _mint(owner, 1_000_000_000e18); + } +} + +contract MockERC20Airdrop2 is ERC20Airdrop2 { + function _verifyMerkleProof( + bytes32[] calldata, /*proof*/ + bytes32, /*merkleRoot*/ + bytes32 /*value*/ + ) + internal + pure + override + returns (bool) + { + return true; + } +} + +contract TestERC20Airdrop2 is TaikoTest { + address public owner = randAddress(); + + bytes32 public constant merkleRoot = bytes32(uint256(1)); + bytes32[] public merkleProof; + uint64 public claimStart; + uint64 public claimEnd; + + ERC20 token; + ERC20Airdrop2 airdrop2; + + function setUp() public { + claimStart = uint64(block.timestamp + 10); + claimEnd = uint64(block.timestamp + 10_000); + merkleProof = new bytes32[](3); + + token = new MyERC20(address(owner)); + + airdrop2 = ERC20Airdrop2( + deployProxy({ + name: "MockERC20Airdrop", + impl: address(new MockERC20Airdrop2()), + data: abi.encodeCall( + ERC20Airdrop2.init, + (claimStart, claimEnd, merkleRoot, address(token), owner, 10 days) + ) + }) + ); + + vm.prank(owner, owner); + MyERC20(address(token)).approve(address(airdrop2), 1_000_000_000e18); + vm.roll(block.number + 1); + } + + function test_withdraw_for_airdrop2_withdraw_daily() public { + vm.warp(uint64(block.timestamp + 11)); + + vm.prank(Alice, Alice); + airdrop2.claim(Alice, 100, merkleProof); + + // Try withdraw but not started yet + vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); + airdrop2.withdraw(Alice); + + // Roll one day after another, for 10 days and see the 100 allowance be withdrawn all and no + // more left for the 11th day + uint256 i = 1; + uint256 balance; + uint256 withdrawable; + for (i = 1; i < 11; i++) { + vm.roll(block.number + 200); + vm.warp(claimEnd + (i * 1 days)); + + (balance, withdrawable) = airdrop2.getBalance(Alice); + + assertEq(balance, 100); + assertEq(withdrawable, 10); + + airdrop2.withdraw(Alice); + // Check Alice balance + assertEq(token.balanceOf(Alice), (i * 10)); + } + + // On the 10th day (midnight), Alice has no claims left + vm.roll(block.number + 200); + vm.warp(claimEnd + (10 days)); + + (balance, withdrawable) = airdrop2.getBalance(Alice); + + assertEq(balance, 100); + assertEq(withdrawable, 0); + + // No effect + airdrop2.withdraw(Alice); + // Check Alice balance + assertEq(token.balanceOf(Alice), 100); + } + + function test_withdraw_for_airdrop2_withdraw_at_the_end() public { + vm.warp(uint64(block.timestamp + 11)); + + vm.prank(Alice, Alice); + airdrop2.claim(Alice, 100, merkleProof); + + // Try withdraw but not started yet + vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); + airdrop2.withdraw(Alice); + + // Roll 10 day after + vm.roll(block.number + 200); + vm.warp(claimEnd + 10 days); + + (uint256 balance, uint256 withdrawable) = airdrop2.getBalance(Alice); + + assertEq(balance, 100); + assertEq(withdrawable, 100); + + airdrop2.withdraw(Alice); + + // Check Alice balance + assertEq(token.balanceOf(Alice), 100); + } + + function test_withdraw_for_airdrop2_but_out_of_withdrawal_window() public { + vm.warp(uint64(block.timestamp + 11)); + + vm.prank(Alice, Alice); + airdrop2.claim(Alice, 100, merkleProof); + + // Try withdraw but not started yet + vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); + airdrop2.withdraw(Alice); + + // Roll 11 day after + vm.roll(block.number + 200); + vm.warp(claimEnd + 11 days); + + (uint256 balance, uint256 withdrawable) = airdrop2.getBalance(Alice); + + // Balance and withdrawable is 100,100 --> bc. it is out of withdrawal window + assertEq(balance, 100); + assertEq(withdrawable, 100); + + vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); + airdrop2.withdraw(Alice); + + // Check Alice balance + assertEq(token.balanceOf(Alice), 0); + } +} diff --git a/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol b/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol index 21d3e949e6..5e29afceec 100644 --- a/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol +++ b/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol @@ -3,243 +3,85 @@ pragma solidity 0.8.24; import "../../TaikoTest.sol"; -contract MyERC20 is ERC20 { - constructor(address owner) ERC20("Taiko Token", "TKO") { - _mint(owner, 1_000_000_000e18); +contract MockMerkleClaimable is MerkleClaimable { + function init(uint64 _claimStart, uint64 _claimEnd, bytes32 _merkleRoot) external initializer { + __MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot); + } + + function verifyClaim(bytes calldata data, bytes32[] calldata proof) external { + _verifyClaim(data, proof); } } -contract TestERC20Airdrop is TaikoTest { - uint64 claimStart; - uint64 claimEnd; - address internal owner = randAddress(); +contract TestMerkleClaimable is TaikoTest { + bytes public data = abi.encode(Alice, 100); - bytes32 merkleRoot = 0x73a7330a8657ad864b954215a8f636bb3709d2edea60bcd4fcb8a448dbc6d70f; + bytes32 public constant merkleRoot = + 0x73a7330a8657ad864b954215a8f636bb3709d2edea60bcd4fcb8a448dbc6d70f; + bytes32[] public merkleProof; + uint64 public claimStart; + uint64 public claimEnd; - ERC20Airdrop airdrop; - ERC20Airdrop2 airdrop2; - ERC20 token; + MockMerkleClaimable public merkleClaimable; function setUp() public { - token = new MyERC20(address(owner)); - // 1st 'genesis' airdrop - airdrop = ERC20Airdrop( - deployProxy({ - name: "airdrop", - impl: address(new ERC20Airdrop()), - data: abi.encodeCall(ERC20Airdrop.init, (0, 0, merkleRoot, address(token), owner)) - }) - ); + claimStart = uint64(block.timestamp + 10); + claimEnd = uint64(block.timestamp + 10_000); + + merkleProof = new bytes32[](3); + merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; + merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; + merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - // 2nd airdrop subject to unlocking (e.g. 10 days after starting after - // claim window) - airdrop2 = ERC20Airdrop2( + merkleClaimable = MockMerkleClaimable( deployProxy({ - name: "airdrop", - impl: address(new ERC20Airdrop2()), - data: abi.encodeCall( - ERC20Airdrop2.init, (0, 0, merkleRoot, address(token), owner, 10 days) - ) + name: "MockMerkleClaimable", + impl: address(new MockMerkleClaimable()), + data: abi.encodeCall(MockMerkleClaimable.init, (0, 0, merkleRoot)) }) ); - claimStart = uint64(block.timestamp + 10); - claimEnd = uint64(block.timestamp + 10_000); - - airdrop.setConfig(claimStart, claimEnd, merkleRoot); - - airdrop2.setConfig(claimStart, claimEnd, merkleRoot); + vm.startPrank(merkleClaimable.owner()); + merkleClaimable.setConfig(claimStart, claimEnd, merkleRoot); vm.roll(block.number + 1); vm.warp(block.timestamp + 12); - - vm.prank(owner, owner); - MyERC20(address(token)).approve(address(airdrop), 1_000_000_000e18); - - vm.prank(owner, owner); - MyERC20(address(token)).approve(address(airdrop2), 1_000_000_000e18); } - function test_claim_but_claim_not_ongoing_yet() public { - vm.warp(1); - bytes32[] memory merkleProof = new bytes32[](3); - merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; - merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; - merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - - vm.expectRevert(MerkleClaimable.CLAIM_NOT_ONGOING.selector); - vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); + function test_verifyClaim_when_it_starts() public { + vm.warp(claimStart); + merkleClaimable.verifyClaim(data, merkleProof); } - function test_claim_but_claim_not_ongoing_anymore() public { - vm.warp(uint64(block.timestamp + 11_000)); - - bytes32[] memory merkleProof = new bytes32[](3); - merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; - merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; - merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - + function test_verifyClaim_before_it_starts() public { + vm.warp(claimStart - 1); vm.expectRevert(MerkleClaimable.CLAIM_NOT_ONGOING.selector); - vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); + merkleClaimable.verifyClaim(data, merkleProof); } - function test_claim_but_with_invalid_allowance() public { - vm.warp(uint64(block.timestamp + 11)); - // These proofs are coming from 'pnpm run buildMerkle' - bytes32[] memory merkleProof = new bytes32[](3); - merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; - merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; - merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - - vm.expectRevert(MerkleClaimable.INVALID_PROOF.selector); - vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 200), merkleProof); + function test_verifyClaim_when_it_ends() public { + vm.warp(claimEnd); + merkleClaimable.verifyClaim(data, merkleProof); } - function test_claim() public { - vm.warp(uint64(block.timestamp + 11)); - // These proofs are coming from 'pnpm run buildMerkle' - bytes32[] memory merkleProof = new bytes32[](3); - merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; - merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; - merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - - vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); - - // Check Alice balance - assertEq(token.balanceOf(Alice), 100); + function test_verifyClaim_after_it_ends() public { + vm.warp(claimEnd + 1); + vm.expectRevert(MerkleClaimable.CLAIM_NOT_ONGOING.selector); + merkleClaimable.verifyClaim(data, merkleProof); } - function test_claim_with_same_proofs_twice() public { - vm.warp(uint64(block.timestamp + 11)); - // These proofs are coming from 'pnpm run buildMerkle' - bytes32[] memory merkleProof = new bytes32[](3); - merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; - merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; - merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - - vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); - - // Check Alice balance - assertEq(token.balanceOf(Alice), 100); + function test_verifyClaim_twice_while_its_ongoing() public { + vm.warp(claimStart); + merkleClaimable.verifyClaim(data, merkleProof); vm.expectRevert(MerkleClaimable.CLAIMED_ALREADY.selector); - vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); - } - - function test_withdraw_for_airdrop2_withdraw_daily() public { - vm.warp(uint64(block.timestamp + 11)); - // These proofs are coming from 'pnpm run buildMerkle' - bytes32[] memory merkleProof = new bytes32[](3); - merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; - merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; - merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - - vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof); - - // Try withdraw but not started yet - vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); - airdrop2.withdraw(Alice); - - // Roll one day after another, for 10 days and see the 100 allowance be withdrawn all and no - // more left for the 11th day - uint256 i = 1; - uint256 balance; - uint256 withdrawable; - for (i = 1; i < 11; i++) { - vm.roll(block.number + 200); - vm.warp(claimEnd + (i * 1 days)); - - (balance, withdrawable) = airdrop2.getBalance(Alice); - - assertEq(balance, 100); - assertEq(withdrawable, 10); - - airdrop2.withdraw(Alice); - // Check Alice balance - assertEq(token.balanceOf(Alice), (i * 10)); - } - - // On the 10th day (midnight), Alice has no claims left - vm.roll(block.number + 200); - vm.warp(claimEnd + (10 days)); - - (balance, withdrawable) = airdrop2.getBalance(Alice); - - assertEq(balance, 100); - assertEq(withdrawable, 0); - - // No effect - airdrop2.withdraw(Alice); - // Check Alice balance - assertEq(token.balanceOf(Alice), 100); - } - - function test_withdraw_for_airdrop2_withdraw_at_the_end() public { - vm.warp(uint64(block.timestamp + 11)); - // These proofs are coming from 'pnpm run buildMerkle' - bytes32[] memory merkleProof = new bytes32[](3); - merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; - merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; - merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - - vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof); - - // Try withdraw but not started yet - vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); - airdrop2.withdraw(Alice); - - // Roll 10 day after - vm.roll(block.number + 200); - vm.warp(claimEnd + 10 days); - - (uint256 balance, uint256 withdrawable) = airdrop2.getBalance(Alice); - - assertEq(balance, 100); - assertEq(withdrawable, 100); - - airdrop2.withdraw(Alice); - - // Check Alice balance - assertEq(token.balanceOf(Alice), 100); + merkleClaimable.verifyClaim(data, merkleProof); } - function test_withdraw_for_airdrop2_but_out_of_withdrawal_window() public { - vm.warp(uint64(block.timestamp + 11)); - // These proofs are coming from 'pnpm run buildMerkle' - bytes32[] memory merkleProof = new bytes32[](3); - merkleProof[0] = 0x4014b456db813d18e801fe3b30bbe14542c9c84caa9a92b643f7f46849283077; - merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; - merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; - - vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof); - - // Try withdraw but not started yet - vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); - airdrop2.withdraw(Alice); - - // Roll 11 day after - vm.roll(block.number + 200); - vm.warp(claimEnd + 11 days); - - (uint256 balance, uint256 withdrawable) = airdrop2.getBalance(Alice); - - // Balance and withdrawable is 100,100 --> bc. it is out of withdrawal window - assertEq(balance, 100); - assertEq(withdrawable, 100); - - vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); - airdrop2.withdraw(Alice); - - // Check Alice balance - assertEq(token.balanceOf(Alice), 0); + function test_verifyClaim_with_invalid_proofs_while_its_ongoing() public { + vm.warp(claimStart); + merkleProof[1] = randBytes32(); + vm.expectRevert(MerkleClaimable.INVALID_PROOF.selector); + merkleClaimable.verifyClaim(data, merkleProof); } }