From 3efd24d7175a3565d5e28daa756068824d5068ad Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 10:16:11 +0800 Subject: [PATCH 01/13] Timelock token pool enables EIP712 signature --- .../contracts/common/EssentialContract.sol | 2 +- .../contracts/team/TimelockTokenPool.sol | 41 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/protocol/contracts/common/EssentialContract.sol b/packages/protocol/contracts/common/EssentialContract.sol index 3f27aa40fde..a4d6dee54a9 100644 --- a/packages/protocol/contracts/common/EssentialContract.sol +++ b/packages/protocol/contracts/common/EssentialContract.sol @@ -84,7 +84,7 @@ abstract contract EssentialContract is UUPSUpgradeable, Ownable2StepUpgradeable, } /// @notice Returns true if the contract is paused, and false otherwise. - /// @return True if paused, false otherwise. + /// @return true if paused, false otherwise. function paused() public view returns (bool) { return __paused == _TRUE; } diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 69ccd58e74e..debb48c0543 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.24; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; import "../common/EssentialContract.sol"; /// @title TimelockTokenPool @@ -22,7 +23,8 @@ import "../common/EssentialContract.sol"; /// - team members, advisors, etc. /// - grant program grantees /// @custom:security-contact security@taiko.xyz -contract TimelockTokenPool is EssentialContract { +contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { + using ECDSAUpgradeable for bytes32; using SafeERC20 for IERC20; struct Grant { @@ -55,6 +57,11 @@ contract TimelockTokenPool is EssentialContract { Grant grant; } + struct Withdrawal { + address recipient; + address to; + } + /// @notice The Taiko token address. address public taikoToken; @@ -101,6 +108,7 @@ contract TimelockTokenPool is EssentialContract { error ALREADY_GRANTED(); error INVALID_GRANT(); error INVALID_PARAM(); + error INVALID_SIGNATURE(); error NOTHING_TO_VOID(); /// @notice Initializes the contract. @@ -118,6 +126,8 @@ contract TimelockTokenPool is EssentialContract { initializer { __Essential_init(_owner); + __EIP712_init("Taiko Timelock Token Pool", "1"); + if (_taikoToken == address(0)) revert INVALID_PARAM(); taikoToken = _taikoToken; @@ -162,14 +172,14 @@ contract TimelockTokenPool is EssentialContract { _withdraw(msg.sender, msg.sender); } - /// @notice Withdraws all withdrawable tokens. + /// @notice Withdraws all withdrawable tokens to a designated address. /// @param _to The address where the granted and unlocked tokens shall be sent to. - /// @param _sig Signature provided by the grant recipient. - function withdraw(address _to, bytes memory _sig) external { + /// @param _sig Signature provided by the recipient. + function withdraw(address _recipient, address _to, bytes memory _sig) external { if (_to == address(0)) revert INVALID_PARAM(); - bytes32 hash = keccak256(abi.encodePacked("Withdraw unlocked Taiko token to: ", _to)); - address recipient = ECDSA.recover(hash, _sig); - _withdraw(recipient, _to); + if (!verifySignature(Withdrawal(_recipient, _to), _sig)) revert INVALID_SIGNATURE(); + + _withdraw(_recipient, _to); } /// @notice Returns the summary of the grant for a given recipient. @@ -206,6 +216,23 @@ contract TimelockTokenPool is EssentialContract { return recipients[_recipient].grant; } + /// @notice Verifies if a withdrawal signature is valid + /// @param _withdrawal The withdrawal request. + /// @return true if the signature is valid, false otherwise. + function verifySignature( + Withdrawal memory _withdrawal, + bytes memory _sig + ) + public + view + returns (bool) + { + bytes32 typed = keccak256("Withdrawal(address recipient,address to)"); + bytes32 hash = + _hashTypedDataV4(keccak256(abi.encode(typed, _withdrawal.recipient, _withdrawal.to))); + return hash.recover(_sig) == _withdrawal.recipient; + } + function _withdraw(address _recipient, address _to) private { Recipient storage r = recipients[_recipient]; From a3db68979a8ffca9568ca80ec78803f07fcab870 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 10:18:53 +0800 Subject: [PATCH 02/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index debb48c0543..d9e74fd211c 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -24,7 +24,7 @@ import "../common/EssentialContract.sol"; /// - grant program grantees /// @custom:security-contact security@taiko.xyz contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { - using ECDSAUpgradeable for bytes32; + using ECDSA for bytes32; using SafeERC20 for IERC20; struct Grant { From 681efd6d7524b98a5f09b9c538c28249ba067747 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 10:19:23 +0800 Subject: [PATCH 03/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index d9e74fd211c..c5cbc680506 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -126,7 +126,7 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { initializer { __Essential_init(_owner); - __EIP712_init("Taiko Timelock Token Pool", "1"); + __EIP712_init("Taiko TimelockTokenPool", "1"); if (_taikoToken == address(0)) revert INVALID_PARAM(); taikoToken = _taikoToken; From d8ab1215c6270741a92a01d51e26f25b898c0d67 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 12:43:35 +0800 Subject: [PATCH 04/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index c5cbc680506..325f93c5d1d 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -142,7 +142,7 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { /// This transaction should happen on a regular basis, e.g., quarterly. /// @param _recipient The grant recipient address. /// @param _grant The grant struct. - function grant(address _recipient, Grant memory _grant) external onlyOwner { + function grant(address _recipient, Grant memory _grant) external onlyOwner nonReentrant { if (_recipient == address(0)) revert INVALID_PARAM(); if (recipients[_recipient].grant.amount != 0) revert ALREADY_GRANTED(); @@ -157,7 +157,7 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { /// granted to the recipient will NOT be voided but are subject to the /// original unlock schedule. /// @param _recipient The grant recipient address. - function void(address _recipient) external onlyOwner { + function void(address _recipient) external onlyOwner nonReentrant { Recipient storage r = recipients[_recipient]; uint128 amountVoided = _voidGrant(r.grant); @@ -168,14 +168,14 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { } /// @notice Withdraws all withdrawable tokens. - function withdraw() external { + function withdraw() external nonReentrant { _withdraw(msg.sender, msg.sender); } /// @notice Withdraws all withdrawable tokens to a designated address. /// @param _to The address where the granted and unlocked tokens shall be sent to. /// @param _sig Signature provided by the recipient. - function withdraw(address _recipient, address _to, bytes memory _sig) external { + function withdraw(address _recipient, address _to, bytes memory _sig) external nonReentrant { if (_to == address(0)) revert INVALID_PARAM(); if (!verifySignature(Withdrawal(_recipient, _to), _sig)) revert INVALID_SIGNATURE(); From 8b5ca41b0ecb0f94dd5e9c447b90706ea8447923 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 12:44:43 +0800 Subject: [PATCH 05/13] Update ERC20Airdrop2.sol --- packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol index 88e0e1d2f70..4473bf55a44 100644 --- a/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol +++ b/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol @@ -87,7 +87,7 @@ contract ERC20Airdrop2 is MerkleClaimable { /// @notice External withdraw function /// @param user User address - function withdraw(address user) external ongoingWithdrawals { + function withdraw(address user) external ongoingWithdrawals nonReentrant { (, uint256 amount) = getBalance(user); withdrawnAmount[user] += amount; IERC20(token).safeTransferFrom(vault, user, amount); From cebef932ae232d1db73b104d129efa91e49c996f Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 21:00:40 +0800 Subject: [PATCH 06/13] Update TimelockTokenPool.sol --- .../contracts/team/TimelockTokenPool.sol | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 325f93c5d1d..6123c235470 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -58,7 +58,6 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { } struct Withdrawal { - address recipient; address to; } @@ -175,11 +174,15 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { /// @notice Withdraws all withdrawable tokens to a designated address. /// @param _to The address where the granted and unlocked tokens shall be sent to. /// @param _sig Signature provided by the recipient. - function withdraw(address _recipient, address _to, bytes memory _sig) external nonReentrant { + function withdraw(address _to, bytes memory _sig) external nonReentrant { if (_to == address(0)) revert INVALID_PARAM(); - if (!verifySignature(Withdrawal(_recipient, _to), _sig)) revert INVALID_SIGNATURE(); - _withdraw(_recipient, _to); + bytes32 typed = keccak256("Withdrawal(address to)"); + bytes32 hash = _hashTypedDataV4(keccak256(abi.encode(typed, _to))); + address recipient = hash.recover(_sig); + if (recipient == address(0)) revert INVALID_SIGNATURE(); + + _withdraw(recipient, _to); } /// @notice Returns the summary of the grant for a given recipient. @@ -216,23 +219,6 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { return recipients[_recipient].grant; } - /// @notice Verifies if a withdrawal signature is valid - /// @param _withdrawal The withdrawal request. - /// @return true if the signature is valid, false otherwise. - function verifySignature( - Withdrawal memory _withdrawal, - bytes memory _sig - ) - public - view - returns (bool) - { - bytes32 typed = keccak256("Withdrawal(address recipient,address to)"); - bytes32 hash = - _hashTypedDataV4(keccak256(abi.encode(typed, _withdrawal.recipient, _withdrawal.to))); - return hash.recover(_sig) == _withdrawal.recipient; - } - function _withdraw(address _recipient, address _to) private { Recipient storage r = recipients[_recipient]; From 2987efaa20335595d066e9fa11b2646f17746729 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 21:01:49 +0800 Subject: [PATCH 07/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 6123c235470..cebe7fbda98 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -173,7 +173,7 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { /// @notice Withdraws all withdrawable tokens to a designated address. /// @param _to The address where the granted and unlocked tokens shall be sent to. - /// @param _sig Signature provided by the recipient. + /// @param _sig Signature provided by the grant recipient. function withdraw(address _to, bytes memory _sig) external nonReentrant { if (_to == address(0)) revert INVALID_PARAM(); From d012812b7e8438e9c5d5737f79239657e939c05a Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 21:02:51 +0800 Subject: [PATCH 08/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index cebe7fbda98..500e8af768f 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -179,6 +179,7 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { bytes32 typed = keccak256("Withdrawal(address to)"); bytes32 hash = _hashTypedDataV4(keccak256(abi.encode(typed, _to))); + address recipient = hash.recover(_sig); if (recipient == address(0)) revert INVALID_SIGNATURE(); From a9c26dd96ef29023aa1c2718cfd838ee0a516da5 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 21:05:29 +0800 Subject: [PATCH 09/13] Update TimelockTokenPool.sol --- .../protocol/contracts/team/TimelockTokenPool.sol | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 500e8af768f..a0dcb21d478 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -24,7 +24,6 @@ import "../common/EssentialContract.sol"; /// - grant program grantees /// @custom:security-contact security@taiko.xyz contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { - using ECDSA for bytes32; using SafeERC20 for IERC20; struct Grant { @@ -61,6 +60,8 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { address to; } + bytes32 public TYPED_HASH = keccak256("Withdrawal(address to)"); + /// @notice The Taiko token address. address public taikoToken; @@ -107,7 +108,6 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { error ALREADY_GRANTED(); error INVALID_GRANT(); error INVALID_PARAM(); - error INVALID_SIGNATURE(); error NOTHING_TO_VOID(); /// @notice Initializes the contract. @@ -176,13 +176,8 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { /// @param _sig Signature provided by the grant recipient. function withdraw(address _to, bytes memory _sig) external nonReentrant { if (_to == address(0)) revert INVALID_PARAM(); - - bytes32 typed = keccak256("Withdrawal(address to)"); - bytes32 hash = _hashTypedDataV4(keccak256(abi.encode(typed, _to))); - - address recipient = hash.recover(_sig); - if (recipient == address(0)) revert INVALID_SIGNATURE(); - + bytes32 hash = _hashTypedDataV4(keccak256(abi.encode(TYPED_HASH, _to))); + address recipient = ECDSA.recover(hash, _sig); _withdraw(recipient, _to); } From 3cfed824163659b614121f37c8da8a2fea545a65 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 21:06:16 +0800 Subject: [PATCH 10/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index a0dcb21d478..7acbe00ee05 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -60,7 +60,7 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { address to; } - bytes32 public TYPED_HASH = keccak256("Withdrawal(address to)"); + bytes32 public constant TYPED_HASH = keccak256("Withdrawal(address to)"); /// @notice The Taiko token address. address public taikoToken; From 7bd85c58b6236e97fd9cf284ecccf0dce3c544fe Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 21:08:10 +0800 Subject: [PATCH 11/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 7acbe00ee05..63b89f3ff67 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -176,11 +176,14 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { /// @param _sig Signature provided by the grant recipient. function withdraw(address _to, bytes memory _sig) external nonReentrant { if (_to == address(0)) revert INVALID_PARAM(); - bytes32 hash = _hashTypedDataV4(keccak256(abi.encode(TYPED_HASH, _to))); - address recipient = ECDSA.recover(hash, _sig); + address recipient = ECDSA.recover(getWithdrawalHash(_to), _sig); _withdraw(recipient, _to); } + function getWithdrawalHash(address _to) public view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(TYPED_HASH, _to))); + } + /// @notice Returns the summary of the grant for a given recipient. function getMyGrantSummary(address _recipient) public From a7fb5880b35e8adf96151d3cb3d39b5f84d20ac6 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 21:08:42 +0800 Subject: [PATCH 12/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 63b89f3ff67..6b1cf4cfc4b 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -56,10 +56,6 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { Grant grant; } - struct Withdrawal { - address to; - } - bytes32 public constant TYPED_HASH = keccak256("Withdrawal(address to)"); /// @notice The Taiko token address. From 3f475af74bf33d60e78bbfe6d3fe4683b856702e Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Wed, 6 Mar 2024 21:11:23 +0800 Subject: [PATCH 13/13] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 6b1cf4cfc4b..18f7d3a3007 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -176,6 +176,9 @@ contract TimelockTokenPool is EssentialContract, EIP712Upgradeable { _withdraw(recipient, _to); } + /// @notice Gets the hash to be signed to authorize an withdrawal. + /// @param _to The destination address. + /// @return The hash to be signed. function getWithdrawalHash(address _to) public view returns (bytes32) { return _hashTypedDataV4(keccak256(abi.encode(TYPED_HASH, _to))); }