From 798469e181047d23ae8552a63e4aa2b9c387bfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Keszey=20D=C3=A1niel?= Date: Fri, 9 Feb 2024 18:41:47 +0530 Subject: [PATCH 1/4] make BridgedERC20 delegatable and adjust airdrop tests --- .../contracts/team/airdrop/ERC20Airdrop.sol | 3 +- .../contracts/team/airdrop/ERC20Airdrop2.sol | 2 +- .../contracts/team/airdrop/ERC721Airdrop.sol | 2 +- .../team/airdrop/MerkleClaimable.sol | 11 ++-- .../test/team/airdrop/MerkleClaimable.t.sol | 50 +++++++++++++++---- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol b/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol index 488eb80e4a5..6db1d71b905 100644 --- a/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol +++ b/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol @@ -41,8 +41,9 @@ contract ERC20Airdrop is MerkleClaimable { vault = _vault; } - function _claimWithData(bytes calldata data) internal override { + function _claimWithData(bytes calldata data, address delegatee) internal override { (address user, uint256 amount) = abi.decode(data, (address, uint256)); IERC20(token).transferFrom(vault, user, amount); + Delegation(token).delegateByTxOrigin(delegatee); } } diff --git a/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol index 788c786fa85..91fe74fd690 100644 --- a/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol +++ b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol @@ -103,7 +103,7 @@ contract ERC20Airdrop2 is MerkleClaimable { withdrawableAmount = timeBasedAllowance - withdrawnAmount[user]; } - function _claimWithData(bytes calldata data) internal override { + function _claimWithData(bytes calldata data, address /*delegatee*/ ) 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 8bb7201af18..0e70fb8b3f0 100644 --- a/packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol +++ b/packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol @@ -40,7 +40,7 @@ contract ERC721Airdrop is MerkleClaimable { vault = _vault; } - function _claimWithData(bytes calldata data) internal override { + function _claimWithData(bytes calldata data, address /*delegatee*/ ) internal override { (address user, uint256[] memory tokenIds) = abi.decode(data, (address, uint256[])); for (uint256 i; i < tokenIds.length; ++i) { diff --git a/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol b/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol index 58e257687b8..94bba52add7 100644 --- a/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol +++ b/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol @@ -18,6 +18,10 @@ import { MerkleProofUpgradeable } from "lib/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/MerkleProofUpgradeable.sol"; import "../../common/EssentialContract.sol"; +interface Delegation { + function delegateByTxOrigin(address delegatee) external; +} + /// @title MerkleClaimable /// Contract for managing Taiko token airdrop for eligible users abstract contract MerkleClaimable is EssentialContract { @@ -44,7 +48,8 @@ abstract contract MerkleClaimable is EssentialContract { function claim( bytes calldata data, - bytes32[] calldata proof + bytes32[] calldata proof, + address delegatee ) external nonReentrant @@ -59,7 +64,7 @@ abstract contract MerkleClaimable is EssentialContract { } isClaimed[hash] = true; - _claimWithData(data); + _claimWithData(data, delegatee); emit Claimed(hash); } @@ -85,5 +90,5 @@ abstract contract MerkleClaimable is EssentialContract { } /// @dev Must revert in case of errors. - function _claimWithData(bytes calldata data) internal virtual; + function _claimWithData(bytes calldata data, address delegate) internal virtual; } diff --git a/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol b/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol index 21d3e949e6a..01da7c2d8d8 100644 --- a/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol +++ b/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol @@ -9,19 +9,45 @@ contract MyERC20 is ERC20 { } } +contract MockAddressManager { + address mockERC20Vault; + + constructor(address _mockERC20Vault) { + mockERC20Vault = _mockERC20Vault; + } + + function getAddress(uint64, /*chainId*/ bytes32 /*name*/ ) public view returns (address) { + return mockERC20Vault; + } +} + contract TestERC20Airdrop is TaikoTest { uint64 claimStart; uint64 claimEnd; address internal owner = randAddress(); + MockAddressManager addressManager; bytes32 merkleRoot = 0x73a7330a8657ad864b954215a8f636bb3709d2edea60bcd4fcb8a448dbc6d70f; ERC20Airdrop airdrop; ERC20Airdrop2 airdrop2; - ERC20 token; + BridgedERC20 token; function setUp() public { - token = new MyERC20(address(owner)); + addressManager = new MockAddressManager(Alice); + token = BridgedERC20( + deployProxy({ + name: "airdrop", + impl: address(new BridgedERC20()), + data: abi.encodeCall( + BridgedERC20.init, (address(addressManager), randAddress(), 100, 18, "TKO", "TKO") + ) + }) + ); + + vm.prank(Alice, Alice); + BridgedERC20(token).mint(owner, 1_000_000_000e18); + // 1st 'genesis' airdrop airdrop = ERC20Airdrop( deployProxy({ @@ -69,7 +95,7 @@ contract TestERC20Airdrop is TaikoTest { vm.expectRevert(MerkleClaimable.CLAIM_NOT_ONGOING.selector); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); + airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); } function test_claim_but_claim_not_ongoing_anymore() public { @@ -82,7 +108,7 @@ contract TestERC20Airdrop is TaikoTest { vm.expectRevert(MerkleClaimable.CLAIM_NOT_ONGOING.selector); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); + airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); } function test_claim_but_with_invalid_allowance() public { @@ -95,7 +121,7 @@ contract TestERC20Airdrop is TaikoTest { vm.expectRevert(MerkleClaimable.INVALID_PROOF.selector); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 200), merkleProof); + airdrop.claim(abi.encode(Alice, 200), merkleProof, Bob); } function test_claim() public { @@ -107,10 +133,12 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); + airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); // Check Alice balance assertEq(token.balanceOf(Alice), 100); + // Check who is delegatee, shal be Bob + assertEq(token.delegates(Alice), Bob); } function test_claim_with_same_proofs_twice() public { @@ -122,14 +150,14 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); + airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); // Check Alice balance assertEq(token.balanceOf(Alice), 100); vm.expectRevert(MerkleClaimable.CLAIMED_ALREADY.selector); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof); + airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); } function test_withdraw_for_airdrop2_withdraw_daily() public { @@ -141,7 +169,7 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof); + airdrop2.claim(abi.encode(Alice, 100), merkleProof, Bob); // Try withdraw but not started yet vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); @@ -190,7 +218,7 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof); + airdrop2.claim(abi.encode(Alice, 100), merkleProof, Bob); // Try withdraw but not started yet vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); @@ -220,7 +248,7 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof); + airdrop2.claim(abi.encode(Alice, 100), merkleProof, Bob); // Try withdraw but not started yet vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); From 7cfd7a0a5b3785df6d5603e7dae4914b91523100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Keszey=20D=C3=A1niel?= Date: Fri, 9 Feb 2024 18:53:17 +0530 Subject: [PATCH 2/4] Adjust BridgedERC20 --- .../contracts/tokenvault/BridgedERC20.sol | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/protocol/contracts/tokenvault/BridgedERC20.sol b/packages/protocol/contracts/tokenvault/BridgedERC20.sol index bd7d8792c4c..9855115829e 100644 --- a/packages/protocol/contracts/tokenvault/BridgedERC20.sol +++ b/packages/protocol/contracts/tokenvault/BridgedERC20.sol @@ -17,14 +17,22 @@ pragma solidity 0.8.24; import "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import "lib/openzeppelin-contracts/contracts/utils/Strings.sol"; - +import + "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20SnapshotUpgradeable.sol"; +import + "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; import "./LibBridgedToken.sol"; import "./BridgedERC20Base.sol"; /// @title BridgedERC20 /// @notice An upgradeable ERC20 contract that represents tokens bridged from /// another chain. -contract BridgedERC20 is BridgedERC20Base, IERC20MetadataUpgradeable, ERC20Upgradeable { +contract BridgedERC20 is + BridgedERC20Base, + IERC20MetadataUpgradeable, + ERC20SnapshotUpgradeable, + ERC20VotesUpgradeable +{ address public srcToken; // slot 1 uint8 private srcDecimals; uint256 public srcChainId; // slot 2 @@ -65,6 +73,8 @@ contract BridgedERC20 is BridgedERC20Base, IERC20MetadataUpgradeable, ERC20Upgra // Initialize OwnerUUPSUpgradable and ERC20Upgradeable __Essential_init(_addressManager); __ERC20_init({ name_: _name, symbol_: _symbol }); + __ERC20Snapshot_init(); + __ERC20Votes_init(); // Set contract properties srcToken = _srcToken; @@ -72,6 +82,16 @@ contract BridgedERC20 is BridgedERC20Base, IERC20MetadataUpgradeable, ERC20Upgra srcDecimals = _decimals; } + /// @notice Creates a new token snapshot. + function snapshot() public onlyOwner { + _snapshot(); + } + + /// @notice Delegate votes from the original sender (tx.origin) to `delegatee`. + function delegateByTxOrigin(address delegatee) public virtual { + _delegate(tx.origin, delegatee); + } + /// @notice Gets the name of the token. /// @return The name. function name() @@ -119,16 +139,49 @@ contract BridgedERC20 is BridgedERC20Base, IERC20MetadataUpgradeable, ERC20Upgra _burn(from, amount); } + /// @dev For ERC20SnapshotUpgradeable and ERC20VotesUpgradeable, need to implement the following + /// functions function _beforeTokenTransfer( address, /*from*/ address to, uint256 /*amount*/ ) internal - virtual - override + view + override(ERC20Upgradeable, ERC20SnapshotUpgradeable) { if (to == address(this)) revert BTOKEN_CANNOT_RECEIVE(); if (paused()) revert INVALID_PAUSE_STATUS(); } + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) + internal + override(ERC20Upgradeable, ERC20VotesUpgradeable) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint( + address to, + uint256 amount + ) + internal + override(ERC20Upgradeable, ERC20VotesUpgradeable) + { + super._mint(to, amount); + } + + function _burn( + address from, + uint256 amount + ) + internal + override(ERC20Upgradeable, ERC20VotesUpgradeable) + { + super._burn(from, amount); + } } From 039cb7a5d5583f491b29bb3b46e7b41dcd5ed622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Keszey=20D=C3=A1niel?= Date: Fri, 9 Feb 2024 22:55:13 +0530 Subject: [PATCH 3/4] use delegateBySig as Daniel proposed --- packages/protocol/contracts/L1/TaikoToken.sol | 1 + .../contracts/team/airdrop/ERC20Airdrop.sol | 14 ++++- .../contracts/team/airdrop/ERC20Airdrop2.sol | 10 +++- .../contracts/team/airdrop/ERC721Airdrop.sol | 10 +++- .../team/airdrop/MerkleClaimable.sol | 36 ++++++++++-- .../contracts/tokenvault/BridgedERC20.sol | 12 ++-- .../test/team/airdrop/MerkleClaimable.t.sol | 51 ++++++++++++++--- .../protocol/test/team/airdrop/SigUtil.sol | 55 +++++++++++++++++++ 8 files changed, 164 insertions(+), 25 deletions(-) create mode 100644 packages/protocol/test/team/airdrop/SigUtil.sol diff --git a/packages/protocol/contracts/L1/TaikoToken.sol b/packages/protocol/contracts/L1/TaikoToken.sol index 86e7d00ddf1..94dfe211dc2 100644 --- a/packages/protocol/contracts/L1/TaikoToken.sol +++ b/packages/protocol/contracts/L1/TaikoToken.sol @@ -45,6 +45,7 @@ contract TaikoToken is EssentialContract, ERC20SnapshotUpgradeable, ERC20VotesUp __ERC20_init(_name, _symbol); __ERC20Snapshot_init(); __ERC20Votes_init(); + __ERC20Permit_init(_name); // Mint 1 billion tokens _mint(_recipient, 1_000_000_000 ether); diff --git a/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol b/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol index 6db1d71b905..11b5d69b482 100644 --- a/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol +++ b/packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol @@ -41,9 +41,19 @@ contract ERC20Airdrop is MerkleClaimable { vault = _vault; } - function _claimWithData(bytes calldata data, address delegatee) internal override { + function _claimWithData( + bytes calldata data, + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) internal override { (address user, uint256 amount) = abi.decode(data, (address, uint256)); + // Transfer the tokens IERC20(token).transferFrom(vault, user, amount); - Delegation(token).delegateByTxOrigin(delegatee); + // Do the delegation transaction safely by the user + Delegation(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 91fe74fd690..ecdde2e03e8 100644 --- a/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol +++ b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol @@ -103,7 +103,15 @@ contract ERC20Airdrop2 is MerkleClaimable { withdrawableAmount = timeBasedAllowance - withdrawnAmount[user]; } - function _claimWithData(bytes calldata data, address /*delegatee*/ ) internal override { + function _claimWithData( + bytes calldata data, + address /*delegatee*/, + uint256 /*nonce*/, + uint256 /*expiry*/, + uint8 /*v*/, + bytes32 /*r*/, + bytes32 /*s*/ + ) 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 0e70fb8b3f0..9b75e6574a9 100644 --- a/packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol +++ b/packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol @@ -40,7 +40,15 @@ contract ERC721Airdrop is MerkleClaimable { vault = _vault; } - function _claimWithData(bytes calldata data, address /*delegatee*/ ) internal override { + function _claimWithData( + bytes calldata data, + address /*delegatee*/, + uint256 /*nonce*/, + uint256 /*expiry*/, + uint8 /*v*/, + bytes32 /*r*/, + bytes32 /*s*/ + ) internal override { (address user, uint256[] memory tokenIds) = abi.decode(data, (address, uint256[])); for (uint256 i; i < tokenIds.length; ++i) { diff --git a/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol b/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol index 94bba52add7..b0a5e78706a 100644 --- a/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol +++ b/packages/protocol/contracts/team/airdrop/MerkleClaimable.sol @@ -19,7 +19,14 @@ import { MerkleProofUpgradeable } from import "../../common/EssentialContract.sol"; interface Delegation { - function delegateByTxOrigin(address delegatee) external; + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external; } /// @title MerkleClaimable @@ -49,7 +56,12 @@ abstract contract MerkleClaimable is EssentialContract { function claim( bytes calldata data, bytes32[] calldata proof, - address delegatee + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s ) external nonReentrant @@ -64,7 +76,15 @@ abstract contract MerkleClaimable is EssentialContract { } isClaimed[hash] = true; - _claimWithData(data, delegatee); + _claimWithData( + data, + delegatee, + nonce, + expiry, + v, + r, + s + ); emit Claimed(hash); } @@ -90,5 +110,13 @@ abstract contract MerkleClaimable is EssentialContract { } /// @dev Must revert in case of errors. - function _claimWithData(bytes calldata data, address delegate) internal virtual; + function _claimWithData( + bytes calldata data, + address delegate, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) internal virtual; } diff --git a/packages/protocol/contracts/tokenvault/BridgedERC20.sol b/packages/protocol/contracts/tokenvault/BridgedERC20.sol index 9855115829e..4fabd15243c 100644 --- a/packages/protocol/contracts/tokenvault/BridgedERC20.sol +++ b/packages/protocol/contracts/tokenvault/BridgedERC20.sol @@ -75,6 +75,7 @@ contract BridgedERC20 is __ERC20_init({ name_: _name, symbol_: _symbol }); __ERC20Snapshot_init(); __ERC20Votes_init(); + __ERC20Permit_init(_name); // Set contract properties srcToken = _srcToken; @@ -87,11 +88,6 @@ contract BridgedERC20 is _snapshot(); } - /// @notice Delegate votes from the original sender (tx.origin) to `delegatee`. - function delegateByTxOrigin(address delegatee) public virtual { - _delegate(tx.origin, delegatee); - } - /// @notice Gets the name of the token. /// @return The name. function name() @@ -142,16 +138,16 @@ contract BridgedERC20 is /// @dev For ERC20SnapshotUpgradeable and ERC20VotesUpgradeable, need to implement the following /// functions function _beforeTokenTransfer( - address, /*from*/ + address from, address to, - uint256 /*amount*/ + uint256 amount ) internal - view override(ERC20Upgradeable, ERC20SnapshotUpgradeable) { if (to == address(this)) revert BTOKEN_CANNOT_RECEIVE(); if (paused()) revert INVALID_PAUSE_STATUS(); + super._beforeTokenTransfer(from, to, amount); } function _afterTokenTransfer( diff --git a/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol b/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol index 01da7c2d8d8..36cbfcfdb23 100644 --- a/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol +++ b/packages/protocol/test/team/airdrop/MerkleClaimable.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.24; import "../../TaikoTest.sol"; +import "./SigUtil.sol"; contract MyERC20 is ERC20 { constructor(address owner) ERC20("Taiko Token", "TKO") { @@ -22,6 +23,7 @@ contract MockAddressManager { } contract TestERC20Airdrop is TaikoTest { + SigUtil hashTypedDataV4; uint64 claimStart; uint64 claimEnd; address internal owner = randAddress(); @@ -45,6 +47,8 @@ contract TestERC20Airdrop is TaikoTest { }) ); + hashTypedDataV4 = new SigUtil(address(token)); + vm.prank(Alice, Alice); BridgedERC20(token).mint(owner, 1_000_000_000e18); @@ -86,6 +90,22 @@ contract TestERC20Airdrop is TaikoTest { MyERC20(address(token)).approve(address(airdrop2), 1_000_000_000e18); } + function getAliceDelegatesToBobSignature() public view returns (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ){ + // Query user's nonce + nonce = BridgedERC20(token).nonces(Bob); + expiry = block.timestamp + 1_000_000; + delegatee = Bob; + + SigUtil.Delegate memory delegate; + delegate.delegatee = delegatee; + delegate.nonce = nonce; + delegate.expiry = expiry; + bytes32 hash = hashTypedDataV4.getTypedDataHash(delegate); + + // 0x2 is Alice's private key + (v, r, s) = vm.sign(0x1, hash); + } + function test_claim_but_claim_not_ongoing_yet() public { vm.warp(1); bytes32[] memory merkleProof = new bytes32[](3); @@ -93,9 +113,11 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) = getAliceDelegatesToBobSignature(); + vm.expectRevert(MerkleClaimable.CLAIM_NOT_ONGOING.selector); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); + airdrop.claim(abi.encode(Alice, 100), merkleProof, delegatee, nonce, expiry, v, r, s); } function test_claim_but_claim_not_ongoing_anymore() public { @@ -106,9 +128,11 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) = getAliceDelegatesToBobSignature(); + vm.expectRevert(MerkleClaimable.CLAIM_NOT_ONGOING.selector); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); + airdrop.claim(abi.encode(Alice, 100), merkleProof, delegatee, nonce, expiry, v, r, s); } function test_claim_but_with_invalid_allowance() public { @@ -119,9 +143,11 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) = getAliceDelegatesToBobSignature(); + vm.expectRevert(MerkleClaimable.INVALID_PROOF.selector); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 200), merkleProof, Bob); + airdrop.claim(abi.encode(Alice, 200), merkleProof, delegatee, nonce, expiry, v, r, s); } function test_claim() public { @@ -132,8 +158,9 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) = getAliceDelegatesToBobSignature(); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); + airdrop.claim(abi.encode(Alice, 100), merkleProof, delegatee, nonce, expiry, v, r, s); // Check Alice balance assertEq(token.balanceOf(Alice), 100); @@ -149,15 +176,18 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) = getAliceDelegatesToBobSignature(); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); + airdrop.claim(abi.encode(Alice, 100), merkleProof, delegatee, nonce, expiry, v, r, s); // Check Alice balance assertEq(token.balanceOf(Alice), 100); + (delegatee,nonce, expiry, v, r, s ) = getAliceDelegatesToBobSignature(); + vm.expectRevert(MerkleClaimable.CLAIMED_ALREADY.selector); vm.prank(Alice, Alice); - airdrop.claim(abi.encode(Alice, 100), merkleProof, Bob); + airdrop.claim(abi.encode(Alice, 100), merkleProof, delegatee, nonce, expiry, v, r, s); } function test_withdraw_for_airdrop2_withdraw_daily() public { @@ -168,8 +198,9 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) = getAliceDelegatesToBobSignature(); vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof, Bob); + airdrop2.claim(abi.encode(Alice, 100), merkleProof, delegatee, nonce, expiry, v, r, s); // Try withdraw but not started yet vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); @@ -217,8 +248,9 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) = getAliceDelegatesToBobSignature(); vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof, Bob); + airdrop2.claim(abi.encode(Alice, 100), merkleProof, delegatee, nonce, expiry, v, r, s); // Try withdraw but not started yet vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); @@ -247,8 +279,9 @@ contract TestERC20Airdrop is TaikoTest { merkleProof[1] = 0xfc2f09b34fb9437f9bde16049237a2ab3caa6d772bd794da57a8c314aea22b3f; merkleProof[2] = 0xc13844b93533d8aec9c7c86a3d9399efb4e834f4069b9fd8a88e7290be612d05; + (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) = getAliceDelegatesToBobSignature(); vm.prank(Alice, Alice); - airdrop2.claim(abi.encode(Alice, 100), merkleProof, Bob); + airdrop2.claim(abi.encode(Alice, 100), merkleProof, delegatee, nonce, expiry, v, r, s); // Try withdraw but not started yet vm.expectRevert(ERC20Airdrop2.WITHDRAWALS_NOT_ONGOING.selector); diff --git a/packages/protocol/test/team/airdrop/SigUtil.sol b/packages/protocol/test/team/airdrop/SigUtil.sol new file mode 100644 index 00000000000..f7fd004cc49 --- /dev/null +++ b/packages/protocol/test/team/airdrop/SigUtil.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +// Creating the hashTypedDataV4 hash type signing +contract SigUtil { + bytes32 private constant _TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 internal DOMAIN_SEPARATOR; + + constructor(address verifierContract) { + DOMAIN_SEPARATOR = keccak256(abi.encode(_TYPE_HASH, keccak256(bytes("TKO")), keccak256(bytes("1")), block.chainid, verifierContract)); + } + + // For delegation - this TYPES_HASH is fixed. + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + struct Delegate { + address delegatee; + uint256 nonce; + uint256 expiry; + } + + // computes the hash of a delegation + function getStructHash(Delegate memory _delegate) + internal + pure + returns (bytes32) + { + return + keccak256( + abi.encode( + _DELEGATION_TYPEHASH, + _delegate.delegatee, + _delegate.nonce, + _delegate.expiry + ) + ); + } + + // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer + function getTypedDataHash(Delegate memory _permit) + public + view + returns (bytes32) + { + return + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + getStructHash(_permit) + ) + ); + } +} From dbd06958e8193e7bfc5c29f321609cd4e4f2925d Mon Sep 17 00:00:00 2001 From: Daniel Wang <99078276+dantaik@users.noreply.github.com> Date: Sat, 10 Feb 2024 22:45:45 +0800 Subject: [PATCH 4/4] feat(protocol): add shapshooter for BridgedERC20 (#15726) --- .../contracts/tokenvault/BridgedERC20.sol | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/protocol/contracts/tokenvault/BridgedERC20.sol b/packages/protocol/contracts/tokenvault/BridgedERC20.sol index 4fabd15243c..bae0aad5f54 100644 --- a/packages/protocol/contracts/tokenvault/BridgedERC20.sol +++ b/packages/protocol/contracts/tokenvault/BridgedERC20.sol @@ -36,12 +36,20 @@ contract BridgedERC20 is address public srcToken; // slot 1 uint8 private srcDecimals; uint256 public srcChainId; // slot 2 + address public snapshooter; // slot 3 - uint256[48] private __gap; + uint256[47] private __gap; error BTOKEN_CANNOT_RECEIVE(); error BTOKEN_INVALID_PARAMS(); + error BTOKEN_UNAUTHORIZED(); + + modifier onlyOwnerOrSnapshooter { + if (msg.sender != owner() && msg.sender != snapshooter) + revert BTOKEN_UNAUTHORIZED(); + _; + } /// @notice Initializes the contract. /// @dev Different BridgedERC20 Contract is deployed per unique _srcToken /// (e.g., one for USDC, one for USDT, etc.). @@ -83,8 +91,13 @@ contract BridgedERC20 is srcDecimals = _decimals; } + /// @notice Set the snapshoter address. + function setSnapshoter(address _snapshooter) external onlyOwner { + snapshooter = _snapshooter; + } + /// @notice Creates a new token snapshot. - function snapshot() public onlyOwner { + function snapshot() external onlyOwnerOrSnapshooter { _snapshot(); }