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..18f7d3a3007 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,7 @@ 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 SafeERC20 for IERC20; struct Grant { @@ -55,6 +56,8 @@ contract TimelockTokenPool is EssentialContract { Grant grant; } + bytes32 public constant TYPED_HASH = keccak256("Withdrawal(address to)"); + /// @notice The Taiko token address. address public taikoToken; @@ -118,6 +121,8 @@ contract TimelockTokenPool is EssentialContract { initializer { __Essential_init(_owner); + __EIP712_init("Taiko TimelockTokenPool", "1"); + if (_taikoToken == address(0)) revert INVALID_PARAM(); taikoToken = _taikoToken; @@ -132,7 +137,7 @@ contract TimelockTokenPool is EssentialContract { /// 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(); @@ -147,7 +152,7 @@ contract TimelockTokenPool is EssentialContract { /// 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); @@ -158,20 +163,26 @@ contract TimelockTokenPool is EssentialContract { } /// @notice Withdraws all withdrawable tokens. - function withdraw() external { + function withdraw() external nonReentrant { _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 { + function withdraw(address _to, bytes memory _sig) external nonReentrant { if (_to == address(0)) revert INVALID_PARAM(); - bytes32 hash = keccak256(abi.encodePacked("Withdraw unlocked Taiko token to: ", _to)); - address recipient = ECDSA.recover(hash, _sig); + address recipient = ECDSA.recover(getWithdrawalHash(_to), _sig); _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))); + } + /// @notice Returns the summary of the grant for a given recipient. function getMyGrantSummary(address _recipient) public 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);