From 1dab8b62ecf25a18646f21d9d7e303b112a9bb3b Mon Sep 17 00:00:00 2001 From: retocrooman Date: Tue, 10 Jan 2023 22:14:30 +0900 Subject: [PATCH 1/4] add v1_1 contracts --- contracts/v1_1/EIP2612.sol | 99 ++++++ contracts/v1_1/EIP3009.sol | 260 ++++++++++++++ contracts/v1_1/EIP712Domain.sol | 66 ++++ contracts/v1_1/FiatTokenV1.sol | 605 ++++++++++++++++++++++++++++++++ 4 files changed, 1030 insertions(+) create mode 100644 contracts/v1_1/EIP2612.sol create mode 100644 contracts/v1_1/EIP3009.sol create mode 100644 contracts/v1_1/EIP712Domain.sol create mode 100644 contracts/v1_1/FiatTokenV1.sol diff --git a/contracts/v1_1/EIP2612.sol b/contracts/v1_1/EIP2612.sol new file mode 100644 index 000000000..125364a2e --- /dev/null +++ b/contracts/v1_1/EIP2612.sol @@ -0,0 +1,99 @@ +/** + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2018-2020 CENTRE SECZ + * Copyright (c) 2022 JPYC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.8.11; + +import "../v1/AbstractFiatTokenV1.sol"; +import "./EIP712Domain.sol"; +import "../util/EIP712.sol"; + +/** + * @dev Forked from https://github.com/centrehq/centre-tokens/blob/37039f00534d3e5148269adf98bd2d42ea9fcfd7/contracts/v2/EIP2612.sol + * Modifications: + * 1. Change solidity version to 0.8.11 + * 2. Make domain separator dynamic by adding function: domainSeparatorV4 + * 3. Add gap + * 4. Change now to block.timestamp + */ + +/** + * @title EIP-2612 + * @notice Provide internal implementation for gas-abstracted approvals + */ +abstract contract EIP2612 is AbstractFiatTokenV1, EIP712Domain { + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + bytes32 public constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + mapping(address => uint256) private _permitNonces; + + /** + * @notice Nonces for permit + * @param owner Token owner's address (Authorizer) + * @return Next nonce + */ + function nonces(address owner) external view returns (uint256) { + return _permitNonces[owner]; + } + + /** + * @notice Verify a signed approval permit and execute if valid + * @param owner Token owner's address (Authorizer) + * @param spender Spender's address + * @param value Amount of allowance + * @param deadline The time at which this expires (unix time) + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function _permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + require(deadline >= block.timestamp, "EIP2612: permit is expired"); + + bytes memory data = abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + _permitNonces[owner]++, + deadline + ); + require( + EIP712.recover(_domainSeparatorV4(), v, r, s, data) == owner, + "EIP2612: invalid signature" + ); + + _approve(owner, spender, value); + } + + uint256[50] private __gap; +} diff --git a/contracts/v1_1/EIP3009.sol b/contracts/v1_1/EIP3009.sol new file mode 100644 index 000000000..d1a5688f0 --- /dev/null +++ b/contracts/v1_1/EIP3009.sol @@ -0,0 +1,260 @@ +/** + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2018-2020 CENTRE SECZ + * Copyright (c) 2022 JPYC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.8.11; + +import "../v1/AbstractFiatTokenV1.sol"; +import "./EIP712Domain.sol"; +import "../util/EIP712.sol"; + +/** + * @dev Forked from https://github.com/centrehq/centre-tokens/blob/37039f00534d3e5148269adf98bd2d42ea9fcfd7/contracts/v2/EIP3009.sol + * Modifications: + * 1. Change solidity version to 0.8.11 + * 2. Make domain separator dynamic by adding function: domainSeparatorV4 + * 3. Change _authorizationStates to uint256 for gas optimization + * 4. Change now to block.timestamp + * 5. Add gap + */ + +/** + * @title EIP-3009 + * @notice Provide internal implementation for gas-abstracted transfers + * @dev Contracts that inherit from this must wrap these with publicly + * accessible functions, optionally adding modifiers where necessary + */ +abstract contract EIP3009 is AbstractFiatTokenV1, EIP712Domain { + // keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") + bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = + 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267; + + // keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") + bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = + 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8; + + // keccak256("CancelAuthorization(address authorizer,bytes32 nonce)") + bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = + 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429; + + /** + * @dev authorizer address => nonce => bool (true if nonce is used) + */ + mapping(address => mapping(bytes32 => uint256)) private _authorizationStates; + + event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce); + event AuthorizationCanceled( + address indexed authorizer, + bytes32 indexed nonce + ); + + /** + * @notice Returns the state of an authorization + * @dev Nonces are randomly generated 32-byte data unique to the + * authorizer's address + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + * @return True if the nonce is used + */ + function authorizationState(address authorizer, bytes32 nonce) + external + view + returns (bool) + { + return _authorizationStates[authorizer][nonce] == 1; + } + + /** + * @notice Execute a transfer with a signed authorization + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function _transferWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + _requireValidAuthorization(from, nonce, validAfter, validBefore); + + bytes memory data = abi.encode( + TRANSFER_WITH_AUTHORIZATION_TYPEHASH, + from, + to, + value, + validAfter, + validBefore, + nonce + ); + require( + EIP712.recover(_domainSeparatorV4(), v, r, s, data) == from, + "EIP3009: invalid signature" + ); + + _markAuthorizationAsUsed(from, nonce); + _transfer(from, to, value); + } + + /** + * @notice Receive a transfer with a signed authorization from the payer + * @dev This has an additional check to ensure that the payee's address + * matches the caller of this function to prevent front-running attacks. + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function _receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + require(to == msg.sender, "EIP3009: caller must be the payee"); + _requireValidAuthorization(from, nonce, validAfter, validBefore); + + bytes memory data = abi.encode( + RECEIVE_WITH_AUTHORIZATION_TYPEHASH, + from, + to, + value, + validAfter, + validBefore, + nonce + ); + require( + EIP712.recover(_domainSeparatorV4(), v, r, s, data) == from, + "EIP3009: invalid signature" + ); + + _markAuthorizationAsUsed(from, nonce); + _transfer(from, to, value); + } + + /** + * @notice Attempt to cancel an authorization + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function _cancelAuthorization( + address authorizer, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + _requireUnusedAuthorization(authorizer, nonce); + + bytes memory data = abi.encode( + CANCEL_AUTHORIZATION_TYPEHASH, + authorizer, + nonce + ); + require( + EIP712.recover(_domainSeparatorV4(), v, r, s, data) == authorizer, + "EIP3009: invalid signature" + ); + + _authorizationStates[authorizer][nonce] = 1; + emit AuthorizationCanceled(authorizer, nonce); + } + + /** + * @notice Check that an authorization is unused + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + */ + function _requireUnusedAuthorization(address authorizer, bytes32 nonce) + private + view + { + require( + _authorizationStates[authorizer][nonce] == 0, + "EIP3009: authorization is used or canceled" + ); + } + + /** + * @notice Check that authorization is valid + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + */ + function _requireValidAuthorization( + address authorizer, + bytes32 nonce, + uint256 validAfter, + uint256 validBefore + ) private view { + require( + block.timestamp > validAfter, + "EIP3009: authorization is not yet valid" + ); + require( + block.timestamp < validBefore, + "EIP3009: authorization is expired" + ); + _requireUnusedAuthorization(authorizer, nonce); + } + + /** + * @notice Mark an authorization as used + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + */ + function _markAuthorizationAsUsed(address authorizer, bytes32 nonce) + private + { + _authorizationStates[authorizer][nonce] = 1; + emit AuthorizationUsed(authorizer, nonce); + } + + uint256[50] private __gap; +} diff --git a/contracts/v1_1/EIP712Domain.sol b/contracts/v1_1/EIP712Domain.sol new file mode 100644 index 000000000..475fc0974 --- /dev/null +++ b/contracts/v1_1/EIP712Domain.sol @@ -0,0 +1,66 @@ +/** + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.8.11; + +import "../util/EIP712.sol"; + +/** + * @dev Forked from https://github.com/centrehq/centre-tokens/blob/37039f00534d3e5148269adf98bd2d42ea9fcfd7/contracts/v2/EIP712Domain.sol + * Modifications: + * 1. Change solidity version to 0.8.11 + * 2. Add 4 new state variables: DOMAIN_SEPARATOR, CHAIN_ID, NAME, VERSION + * 3. Add new function _domainSeparatorV4 + * 4. Add gap + */ + +/** + * @title EIP712 Domain + */ +contract EIP712Domain { + /** + * @dev EIP712 Domain Separator + */ + bytes32 internal _CACHED_DOMAIN_SEPARATOR; + uint256 internal _CACHED_CHAIN_ID; + string internal _CACHED_NAME; + string internal _CACHED_VERSION; + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if(block.chainid == _CACHED_CHAIN_ID) { + return _CACHED_DOMAIN_SEPARATOR; + } else { + return EIP712.makeDomainSeparator(_CACHED_NAME, _CACHED_VERSION); + } + } + + function DOMAIN_SEPARATOR() external view returns(bytes32) { + return _domainSeparatorV4(); + } + + uint256[50] private __gap; +} \ No newline at end of file diff --git a/contracts/v1_1/FiatTokenV1.sol b/contracts/v1_1/FiatTokenV1.sol new file mode 100644 index 000000000..d3609e62a --- /dev/null +++ b/contracts/v1_1/FiatTokenV1.sol @@ -0,0 +1,605 @@ +/** + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2018-2020 CENTRE SECZ + * Copyright (c) 2022 JPYC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.8.11; + +import "../v1/Ownable.sol"; +import "../v1/Pausable.sol"; +import "../v1/Blocklistable.sol"; +import "../util/EIP712.sol"; +import "../v1/Rescuable.sol"; +import "./EIP3009.sol"; +import "./EIP2612.sol"; +import "../upgradeability/UUPSUpgradeable.sol"; + +/** + * @dev ERC20 Token backed by fiat reserves. Forked from + * https://github.com/centrehq/centre-tokens/blob/37039f00534d3e5148269adf98bd2d42ea9fcfd7/contracts/v1/FiatTokenV1.sol, + * https://github.com/centrehq/centre-tokens/blob/37039f00534d3e5148269adf98bd2d42ea9fcfd7/contracts/v1.1/FiatTokenV1_1.sol, + * https://github.com/centrehq/centre-tokens/blob/37039f00534d3e5148269adf98bd2d42ea9fcfd7/contracts/v2/FiatTokenV2.sol, + * https://github.com/centrehq/centre-tokens/blob/37039f00534d3e5148269adf98bd2d42ea9fcfd7/contracts/v2/FiatTokenV2_1.sol + * Modifications: + * 1. Change solidity version to 0.8.11 + * 2. Use cashe for gas optimization + * 3. Let initialize function initialize a rescuer + * 4. Change materMinter -> minterAdmin + * 5. Use initializedVersion to manage the version + * 6. Check if the approved amount is max amount for gas optimization + */ + +/** + * @title FiatToken + * @dev ERC20 Token backed by fiat reserves + */ +contract FiatTokenV1_1 is + Ownable, + Pausable, + Blocklistable, + Rescuable, + EIP3009, + EIP2612, + UUPSUpgradeable +{ + string public name; + string public symbol; + string public currency; + uint256 internal totalSupply_; + address public minterAdmin; + uint8 public decimals; + uint8 public version; + + mapping(address => uint256) internal balances; + mapping(address => mapping(address => uint256)) internal allowed; + mapping(address => bool) internal minters; + mapping(address => uint256) internal minterAllowed; + + event Mint(address indexed minter, address indexed to, uint256 amount); + event Burn(address indexed burner, uint256 amount); + event MinterConfigured(address indexed minter, uint256 minterAllowedAmount); + event MinterRemoved(address indexed oldMinter); + event MinterAdminChanged(address indexed newMinterAdmin); + + function initialize( + string memory tokenName, + string memory tokenSymbol, + string memory tokenCurrency, + uint8 tokenDecimals, + address newMinterAdmin, + address newPauser, + address newBlocklister, + address newRescuer, + address newOwner + ) public { + require( + version == 0, + "FiatToken: contract is already initialized" + ); + require( + newMinterAdmin != address(0), + "FiatToken: new minterAdmin is the zero address" + ); + require( + newPauser != address(0), + "FiatToken: new pauser is the zero address" + ); + require( + newBlocklister != address(0), + "FiatToken: new blocklister is the zero address" + ); + require( + newRescuer != address(0), + "FiatToken: new rescuer is the zero address" + ); + require( + newOwner != address(0), + "FiatToken: new owner is the zero address" + ); + + name = tokenName; + symbol = tokenSymbol; + currency = tokenCurrency; + decimals = tokenDecimals; + minterAdmin = newMinterAdmin; + pauser = newPauser; + blocklister = newBlocklister; + rescuer = newRescuer; + _transferOwnership(newOwner); + blocklisted[address(this)] = 1; + _CACHED_DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(tokenName, "1"); + _CACHED_CHAIN_ID = block.chainid; + _CACHED_NAME = tokenName; + _CACHED_VERSION = "1"; + version = 1; + } + + /** + * @dev Throws if called by any account other than a minter + */ + modifier onlyMinters() { + require(minters[msg.sender], "FiatToken: caller is not a minter"); + _; + } + + /** + * @dev Function to mint tokens + * @param _to The address that will receive the minted tokens. + * @param _amount The amount of tokens to mint. Must be less than or equal + * to the minterAllowance of the caller. + * @return A boolean that indicates if the operation was successful. + */ + function mint(address _to, uint256 _amount) + external + whenNotPaused + onlyMinters + notBlocklisted(msg.sender) + notBlocklisted(_to) + returns (bool) + { + require(_to != address(0), "FiatToken: mint to the zero address"); + require(_amount > 0, "FiatToken: mint amount not greater than 0"); + + uint256 mintingAllowedAmount = minterAllowed[msg.sender]; + require( + _amount <= mintingAllowedAmount, + "FiatToken: mint amount exceeds minterAllowance" + ); + + totalSupply_ = totalSupply_ + _amount; + balances[_to] = balances[_to] + _amount; + minterAllowed[msg.sender] = mintingAllowedAmount - _amount; + emit Mint(msg.sender, _to, _amount); + emit Transfer(address(0), _to, _amount); + return true; + } + + /** + * @dev Throws if called by any account other than the minterAdmin + */ + modifier onlyMinterAdmin() { + require( + msg.sender == minterAdmin, + "FiatToken: caller is not the minterAdmin" + ); + _; + } + + /** + * @dev Get minter allowance for an account + * @param minter The address of the minter + * @return Allowance of the minter can mint + */ + function minterAllowance(address minter) external view returns (uint256) { + return minterAllowed[minter]; + } + + /** + * @dev Checks if account is a minter + * @param account The address to check + * @return True if account is a minter + */ + function isMinter(address account) external view returns (bool) { + return minters[account]; + } + + /** + * @notice Amount of remaining tokens spender is allowed to transfer on + * behalf of the token owner + * @param owner Token owner's address + * @param spender Spender's address + * @return Allowance amount + */ + function allowance(address owner, address spender) + external + view + override + returns (uint256) + { + return allowed[owner][spender]; + } + + /** + * @dev Get totalSupply of token + * @return TotalSupply + */ + function totalSupply() external view override returns (uint256) { + return totalSupply_; + } + + /** + * @dev Get token balance of an account + * @param account address The account + * @return Balance amount of the account + */ + function balanceOf(address account) + external + view + override + returns (uint256) + { + return balances[account]; + } + + /** + * @notice Set spender's allowance over the caller's tokens to be a given + * value. + * @param spender Spender's address + * @param value Allowance amount + * @return True if successful + */ + function approve(address spender, uint256 value) + external + override + whenNotPaused + notBlocklisted(msg.sender) + notBlocklisted(spender) + returns (bool) + { + _approve(msg.sender, spender, value); + return true; + } + + /** + * @dev Internal function to set allowance + * @param owner Token owner's address + * @param spender Spender's address + * @param value Allowance amount + */ + function _approve( + address owner, + address spender, + uint256 value + ) internal override { + require(owner != address(0), "FiatToken: approve from the zero address"); + require(spender != address(0), "FiatToken: approve to the zero address"); + allowed[owner][spender] = value; + emit Approval(owner, spender, value); + } + + /** + * @notice Transfer tokens by spending allowance + * @param from Payer's address + * @param to Payee's address + * @param value Transfer amount + * @return True if successful + */ + function transferFrom( + address from, + address to, + uint256 value + ) + external + override + whenNotPaused + notBlocklisted(msg.sender) + notBlocklisted(from) + notBlocklisted(to) + returns (bool) + { + uint256 _allowed = allowed[from][msg.sender]; + if (_allowed != type(uint256).max) { + require(_allowed >= value, "FiatToken: transfer amount exceeds allowance"); + allowed[from][msg.sender] = _allowed - value; + } + _transfer(from, to, value); + return true; + } + + /** + * @notice Transfer tokens from the caller + * @param to Payee's address + * @param value Transfer amount + * @return True if successful + */ + function transfer(address to, uint256 value) + external + override + whenNotPaused + notBlocklisted(msg.sender) + notBlocklisted(to) + returns (bool) + { + _transfer(msg.sender, to, value); + return true; + } + + /** + * @notice Internal function to process transfers + * @param from Payer's address + * @param to Payee's address + * @param value Transfer amount + */ + function _transfer( + address from, + address to, + uint256 value + ) internal override { + require(from != address(0), "FiatToken: transfer from the zero address"); + require(to != address(0), "FiatToken: transfer to the zero address"); + uint256 _balances = balances[from]; + require( + value <= _balances, + "FiatToken: transfer amount exceeds balance" + ); + + balances[from] = _balances - value; + balances[to] = balances[to] + value; + emit Transfer(from, to, value); + } + + /** + * @dev Function to add/update a new minter + * @param minter The address of the minter + * @param minterAllowedAmount The minting amount allowed for the minter + * @return True if the operation was successful. + */ + function configureMinter(address minter, uint256 minterAllowedAmount) + external + whenNotPaused + onlyMinterAdmin + returns (bool) + { + minters[minter] = true; + minterAllowed[minter] = minterAllowedAmount; + emit MinterConfigured(minter, minterAllowedAmount); + return true; + } + + /** + * @dev Function to remove a minter + * @param minter The address of the minter to remove + * @return True if the operation was successful. + */ + function removeMinter(address minter) + external + onlyMinterAdmin + returns (bool) + { + minters[minter] = false; + minterAllowed[minter] = 0; + emit MinterRemoved(minter); + return true; + } + + /** + * @dev allows a minter to burn some of its own tokens + * Validates that caller is a minter and that sender is not blocklisted + * amount is less than or equal to the minter's account balance + * @param _amount uint256 the amount of tokens to be burned + */ + function burn(uint256 _amount) + external + whenNotPaused + onlyMinters + notBlocklisted(msg.sender) + { + uint256 balance = balances[msg.sender]; + require(_amount > 0, "FiatToken: burn amount not greater than 0"); + require(balance >= _amount, "FiatToken: burn amount exceeds balance"); + + totalSupply_ = totalSupply_ - _amount; + balances[msg.sender] = balance - _amount; + emit Burn(msg.sender, _amount); + emit Transfer(msg.sender, address(0), _amount); + } + + function updateMinterAdmin(address _newMinterAdmin) external onlyOwner { + require( + _newMinterAdmin != address(0), + "FiatToken: new minterAdmin is the zero address" + ); + minterAdmin = _newMinterAdmin; + emit MinterAdminChanged(minterAdmin); + } + + /** + * @notice Increase the allowance by a given increment + * @param spender Spender's address + * @param increment Amount of increase in allowance + * @return True if successful + */ + function increaseAllowance(address spender, uint256 increment) + external + whenNotPaused + notBlocklisted(msg.sender) + notBlocklisted(spender) + returns (bool) + { + _increaseAllowance(msg.sender, spender, increment); + return true; + } + + /** + * @notice Decrease the allowance by a given decrement + * @param spender Spender's address + * @param decrement Amount of decrease in allowance + * @return True if successful + */ + function decreaseAllowance(address spender, uint256 decrement) + external + whenNotPaused + notBlocklisted(msg.sender) + notBlocklisted(spender) + returns (bool) + { + _decreaseAllowance(msg.sender, spender, decrement); + return true; + } + + /** + * @notice Internal function to increase the allowance by a given increment + * @param owner Token owner's address + * @param spender Spender's address + * @param increment Amount of increase + */ + function _increaseAllowance( + address owner, + address spender, + uint256 increment + ) internal override { + _approve(owner, spender, allowed[owner][spender] + increment); + } + + /** + * @notice Internal function to decrease the allowance by a given decrement + * @param owner Token owner's address + * @param spender Spender's address + * @param decrement Amount of decrease + */ + function _decreaseAllowance( + address owner, + address spender, + uint256 decrement + ) internal override { + uint256 _allowed = allowed[owner][spender]; + require( + decrement <= _allowed, + "FiatToken: decreased allowance below zero" + ); + _approve(owner, spender, _allowed - decrement); + } + + /** + * @notice Execute a transfer with a signed authorization + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function transferWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external whenNotPaused notBlocklisted(from) notBlocklisted(to) { + _transferWithAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s + ); + } + + /** + * @notice Receive a transfer with a signed authorization from the payer + * @dev This has an additional check to ensure that the payee's address + * matches the caller of this function to prevent front-running attacks. + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external whenNotPaused notBlocklisted(from) notBlocklisted(to) { + _receiveWithAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s + ); + } + + /** + * @notice Attempt to cancel an authorization + * @dev Works only if the authorization is not yet used. + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function cancelAuthorization( + address authorizer, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external whenNotPaused { + _cancelAuthorization(authorizer, nonce, v, r, s); + } + + /** + * @notice Update allowance with a signed permit + * @param owner Token owner's address (Authorizer) + * @param spender Spender's address + * @param value Amount of allowance + * @param deadline Expiration time, seconds since the epoch + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external whenNotPaused notBlocklisted(owner) notBlocklisted(spender) { + _permit(owner, spender, value, deadline, v, r, s); + } + + function _authorizeUpgrade(address newImplementation) + internal + override + onlyOwner + {} + + uint256[50] private __gap; +} From ac991ca5ddb54915b6a1b2db002b8bf0a7393dcc Mon Sep 17 00:00:00 2001 From: retocrooman Date: Wed, 11 Jan 2023 15:38:40 +0900 Subject: [PATCH 2/4] fix eip712 domain separator --- contracts/README.md | 10 + ...kenV1Test.sol => FiatTokenV1Test copy.sol} | 2 - contracts/test/FiatTokenV1_1Test.sol | 33 + contracts/v1_1/EIP712Domain.sol | 8 +- .../{FiatTokenV1.sol => FiatTokenV1_1.sol} | 0 test/README.md | 15 +- test/storageSlot/storageSlot.test.js | 4 + test/storageSlot/storageSlots.behavior.js | 7 + test/v1_1/EIP2612.behavior.js | 192 ++++++ test/v1_1/EIP3009.behavior.js | 566 ++++++++++++++++++ test/v1_1/EIP712Domain.behavior.js | 27 + test/v1_1/FiatTokenV1_1.test.js | 232 +++++++ test/v1_1_proxy/FiatTokenV1_1_proxy.test.js | 239 ++++++++ 13 files changed, 1329 insertions(+), 6 deletions(-) rename contracts/test/{FiatTokenV1Test.sol => FiatTokenV1Test copy.sol} (99%) create mode 100644 contracts/test/FiatTokenV1_1Test.sol rename contracts/v1_1/{FiatTokenV1.sol => FiatTokenV1_1.sol} (100%) create mode 100644 test/v1_1/EIP2612.behavior.js create mode 100644 test/v1_1/EIP3009.behavior.js create mode 100644 test/v1_1/EIP712Domain.behavior.js create mode 100644 test/v1_1/FiatTokenV1_1.test.js create mode 100644 test/v1_1_proxy/FiatTokenV1_1_proxy.test.js diff --git a/contracts/README.md b/contracts/README.md index a59e0261d..2a8bb26d7 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -16,6 +16,7 @@ - ERC20.sol @openzepplin + v0.8.11 - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol - FiatTokenV1Test.sol @original + v0.8.11 +- FiatTokenV1_1Test.sol @original + v0.8.11 - FiatTokenV2Test.sol @original + v0.8.11 - IERC20Metadata.sol @openzeppelin + v0.8.11 - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -70,6 +71,15 @@ - Rescuable.sol @centre-tokens + v0.8.11 + gap - https://github.com/centrehq/centre-tokens/blob/master/contracts/v1.1/Rescuable.sol +## v1_1 +- EIP712Domain.sol @fork-centre-tokens + v0.8.11 + gap + - https://github.com/centrehq/centre-tokens/blob/master/contracts/v2/EIP712Domain.sol +- EIP2612.sol @centre-tokens + v0.8.11 + gap + - https://github.com/centrehq/centre-tokens/blob/master/contracts/v2/EIP2612.sol +- EIP3009.sol @centre-tokens + v0.8.11 + gap + - https://github.com/centrehq/centre-tokens/blob/master/contracts/v2/EIP3009.sol +- FiatTokenV1_1.sol @original + v0.8.11 + ## v2 - FiatTokenV2.sol @original + v0.8.11 - FiatTokenV2test.sol @original + v0.8.11 \ No newline at end of file diff --git a/contracts/test/FiatTokenV1Test.sol b/contracts/test/FiatTokenV1Test copy.sol similarity index 99% rename from contracts/test/FiatTokenV1Test.sol rename to contracts/test/FiatTokenV1Test copy.sol index 0e3700e2c..fa3934ba7 100644 --- a/contracts/test/FiatTokenV1Test.sol +++ b/contracts/test/FiatTokenV1Test copy.sol @@ -1,5 +1,3 @@ - - /** * SPDX-License-Identifier: MIT * diff --git a/contracts/test/FiatTokenV1_1Test.sol b/contracts/test/FiatTokenV1_1Test.sol new file mode 100644 index 000000000..d9ff6b860 --- /dev/null +++ b/contracts/test/FiatTokenV1_1Test.sol @@ -0,0 +1,33 @@ +/** + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2018 zOS Global Limited. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.8.11; + +import "../v1_1/FiatTokenV1_1.sol"; + +contract FiatTokenV1_1Test is FiatTokenV1_1 { + function approveTest(address owner, address spender, uint256 value) external { + _approve(owner, spender, value); + } +} \ No newline at end of file diff --git a/contracts/v1_1/EIP712Domain.sol b/contracts/v1_1/EIP712Domain.sol index 475fc0974..ec8167f75 100644 --- a/contracts/v1_1/EIP712Domain.sol +++ b/contracts/v1_1/EIP712Domain.sol @@ -30,9 +30,11 @@ import "../util/EIP712.sol"; * @dev Forked from https://github.com/centrehq/centre-tokens/blob/37039f00534d3e5148269adf98bd2d42ea9fcfd7/contracts/v2/EIP712Domain.sol * Modifications: * 1. Change solidity version to 0.8.11 - * 2. Add 4 new state variables: DOMAIN_SEPARATOR, CHAIN_ID, NAME, VERSION - * 3. Add new function _domainSeparatorV4 - * 4. Add gap + * 2. remove state variable: DOMAIN_SEPARATOR + * 3. Add 4 new state variables: _CACHED_DOMAIN_SEPARATOR, _CACHED_CHAIN_ID, _CACHED_NAME, _CACHED_VERSION + * 4. Add new function _domainSeparatorV4 + * 5. Add new function DOMAIN_SEPARATOR + * 6. Add gap */ /** diff --git a/contracts/v1_1/FiatTokenV1.sol b/contracts/v1_1/FiatTokenV1_1.sol similarity index 100% rename from contracts/v1_1/FiatTokenV1.sol rename to contracts/v1_1/FiatTokenV1_1.sol diff --git a/test/README.md b/test/README.md index a08c9f681..62951f209 100644 --- a/test/README.md +++ b/test/README.md @@ -32,6 +32,7 @@ ### storageSlot.test.js - v1 +- v1_1(upgraded) - v2(upgraded) ### storageSlot.behavior.js @@ -388,4 +389,16 @@ - reject upgradeToAndCall not through delegatecall ## v1_proxy test -- all test of v1 \ No newline at end of file +- all test of v1 except the list below + - shouldBehaveLikeUUPSUpgradeable + +## v1_1 test +- all test of v1 except the list below + - domain separator(v1/EIP2612.behavior.js) + - domain separator(v1/EIP3009.behavior.js) +- add list below + - DOMAIN_SEPARATOR(v1_1/EIP712Domain.behavior.js) + +## v1_1 proxy +- all test of v1_1 except the list below + - shouldBehaveLikeUUPSUpgradeable \ No newline at end of file diff --git a/test/storageSlot/storageSlot.test.js b/test/storageSlot/storageSlot.test.js index c6d5d45b3..ebf49744c 100644 --- a/test/storageSlot/storageSlot.test.js +++ b/test/storageSlot/storageSlot.test.js @@ -6,6 +6,10 @@ contract('FiatTokenV1', (accounts) => { usesOriginalStorageSlotPositions({ version: 1, accounts }) }) +contract('FiatTokenV1_1', (accounts) => { + usesOriginalStorageSlotPositions({ version: 1.1, accounts }) +}) + contract('FiatTokenV2', (accounts) => { usesOriginalStorageSlotPositions({ version: 2, accounts }) }) diff --git a/test/storageSlot/storageSlots.behavior.js b/test/storageSlot/storageSlots.behavior.js index dba17d298..0fb0bdcc5 100644 --- a/test/storageSlot/storageSlots.behavior.js +++ b/test/storageSlot/storageSlots.behavior.js @@ -18,6 +18,7 @@ const { EIP3009TransferMake } = require('../helpers/EIP3009Maker') const FiatTokenProxy = artifacts.require('ERC1967Proxy') const FiatTokenV1 = artifacts.require('FiatTokenV1') +const FiatTokenV1_1 = artifacts.require('FiatTokenV1_1') const FiatTokenV2 = artifacts.require('FiatTokenV2') const { EIP712Domain, domainSeparator } = require('../helpers/eip712'); @@ -61,6 +62,12 @@ function usesOriginalStorageSlotPositions({ version, accounts }) { proxyAsFiatToken = await FiatTokenV1.at(proxy.address) + if (version == 1.1) { + fiatToken = await FiatTokenV1_1.new() + await proxyAsFiatToken.upgradeTo(fiatToken.address, { from: owner }) + proxyAsFiatToken = await FiatTokenV1_1.at(proxy.address); + } + if (version == 2) { fiatToken = await FiatTokenV2.new() await proxyAsFiatToken.upgradeTo(fiatToken.address, { from: owner }) diff --git a/test/v1_1/EIP2612.behavior.js b/test/v1_1/EIP2612.behavior.js new file mode 100644 index 000000000..9ad25905c --- /dev/null +++ b/test/v1_1/EIP2612.behavior.js @@ -0,0 +1,192 @@ +const { + BN, + constants, + expectEvent, + expectRevert, + time, +} = require('@openzeppelin/test-helpers') +const { expect } = require('chai') +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants + +const { fromRpcSig } = require('ethereumjs-util') +const ethSigUtil = require('eth-sig-util') +const Wallet = require('ethereumjs-wallet').default + +const { EIP712Domain, domainSeparator } = require('../helpers/eip712') + +const permitTypeHash = web3.utils.keccak256( + 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)' +) + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +] + +function shouldBehaveLikeEIP2612( + errorPrefix, + name, + initialHolder, + recipient, + pauser, + blocklister, + version="1" +) { + const spender = recipient + + + beforeEach(async function () { + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = 1337 // hardhat.confing.js + }) + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0') + }) + + it.skip('domain separator', async function () { + expect(await this.token._domainSeparatorV4()).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address) + ) + }) + + it('expected permit type hash', async function () { + expect(await this.token.PERMIT_TYPEHASH()).to.equal(permitTypeHash) + }) + + describe('permit', function () { + const wallet = Wallet.generate() + + const owner = wallet.getAddressString() + const value = new BN(42) + const nonce = 0 + const maxDeadline = MAX_UINT256 + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }) + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address) + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { + data, + }) + const { v, r, s } = fromRpcSig(signature) + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s) + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1') + expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal( + value + ) + }) + + it('revert not match given parameters', async function () { + const data = buildData(this.chainId, this.token.address) + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { + data, + }) + const { v, r, s } = fromRpcSig(signature) + + await expectRevert( + this.token.permit(owner, spender, value * 2, maxDeadline, v, r, s), + 'EIP2612: invalid signature' + ) + }) + + it('revert reused signature', async function () { + const data = buildData(this.chainId, this.token.address) + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { + data, + }) + const { v, r, s } = fromRpcSig(signature) + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s) + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'EIP2612: invalid signature' + ) + }) + + it('revert other signature', async function () { + const otherWallet = Wallet.generate() + const data = buildData(this.chainId, this.token.address) + const signature = ethSigUtil.signTypedMessage( + otherWallet.getPrivateKey(), + { data } + ) + const { v, r, s } = fromRpcSig(signature) + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'EIP2612: invalid signature' + ) + }) + + it('revert expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1) + + const data = buildData(this.chainId, this.token.address, deadline) + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { + data, + }) + const { v, r, s } = fromRpcSig(signature) + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'EIP2612: permit is expired' + ) + }) + + it('revert when paused', async function () { + const data = buildData(this.chainId, this.token.address) + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { + data, + }) + const { v, r, s } = fromRpcSig(signature) + + await this.token.pause({ from: pauser }) + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'Pausable: paused' + ) + }) + + it('revert when owner or spender is blocklisted', async function () { + const data = buildData(this.chainId, this.token.address) + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { + data, + }) + const { v, r, s } = fromRpcSig(signature) + + await this.token.blocklist(owner, { from: blocklister }) + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'Blocklistable: account is blocklisted' + ) + + await this.token.unBlocklist(owner, { from: blocklister }) + await this.token.blocklist(spender, { from: blocklister }) + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'Blocklistable: account is blocklisted' + ) + }) + }) +} + +module.exports = { + shouldBehaveLikeEIP2612, +} diff --git a/test/v1_1/EIP3009.behavior.js b/test/v1_1/EIP3009.behavior.js new file mode 100644 index 000000000..5654fa586 --- /dev/null +++ b/test/v1_1/EIP3009.behavior.js @@ -0,0 +1,566 @@ +const crypto = require("crypto"); +const { BN, constants, expectEvent, expectRevert, time} = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig, toChecksumAddress } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { EIP712Domain, domainSeparator } = require('../helpers/eip712'); +const { web3 } = require("@openzeppelin/test-helpers/src/setup"); + +const transferWithAuthorizationTypeHash = web3.utils.keccak256( + "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" +); + +const receiveWithAuthorizationTypeHash = web3.utils.keccak256( + "ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" +); + +const cancelAuthorizationTypeHash = web3.utils.keccak256( + "CancelAuthorization(address authorizer,bytes32 nonce)" +); + +const TransferWithAuthorization = [ + { name: 'from', type: 'address' }, + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'validAfter', type: 'uint256' }, + { name: 'validBefore', type: 'uint256' }, + { name: 'nonce', type: 'bytes32' }, +]; + +const ReceiveWithAuthorization = [ + { name: 'from', type: 'address' }, + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'validAfter', type: 'uint256' }, + { name: 'validBefore', type: 'uint256' }, + { name: 'nonce', type: 'bytes32' }, +]; + +const CancelAuthorization = [ + { name: 'authorizer', type: 'address' }, + { name: 'nonce', type: 'bytes32' }, +]; + +function shouldBehaveLikeEIP3009(errorPrefix, name, initialSupply, initialHolder, recipient, pauser, blocklister, version="1") { + const to = recipient; + + beforeEach(async function () { + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = 1337; // hardhat.confing.js + }); + + it.skip('domain separator', async function () { + expect( + await this.token._domainSeparatorV4(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + it('expected transfer type hash', async function () { + expect(await this.token.TRANSFER_WITH_AUTHORIZATION_TYPEHASH()).to.equal(transferWithAuthorizationTypeHash); + }); + + it('expected receive type hash', async function () { + expect(await this.token.RECEIVE_WITH_AUTHORIZATION_TYPEHASH()).to.equal(receiveWithAuthorizationTypeHash); + }); + + it('expected cancel type hash', async function () { + expect(await this.token.CANCEL_AUTHORIZATION_TYPEHASH()).to.equal(cancelAuthorizationTypeHash); + }); + + describe('TransferWithAuthorization', async function () { + const wallet = Wallet.generate(); + let owner = wallet.getAddressString(); + const from = toChecksumAddress(owner); + + beforeEach(async function () { + await this.token.transfer(from, initialSupply, {from: initialHolder}); + }) + + const value = new BN(42); + const minValidAfter = new BN(0); + const maxValidBefore = MAX_UINT256; + const nonce = "0x" + crypto.randomBytes(32).toString('hex'); + + const buildData = (chainId, verifyingContract, validAfter = minValidAfter, validBefore = maxValidBefore) => ({ + primaryType: 'TransferWithAuthorization', + types: { EIP712Domain, TransferWithAuthorization }, + domain: { name, version, chainId, verifyingContract }, + message: { from, to, value, validAfter, validBefore, nonce }, + }); + + it('accepts owner signature', async function () { + expect(await this.token.balanceOf(from)).to.be.bignumber.equal(initialSupply); + expect(await this.token.balanceOf(to)).to.be.bignumber.equal(new BN(0)); + + expect(await this.token.authorizationState(from, nonce)).to.equal(false); + + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s); + + expect(await this.token.balanceOf(from)).to.be.bignumber.equal(initialSupply.sub(value)); + expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + + expect(await this.token.authorizationState(from, nonce)).to.equal(true); + }); + + it('emits a AuthorizationUsed event', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const result = await this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s); + + expectEvent(result, 'AuthorizationUsed', { + authorizer: from, + nonce: nonce + }); + }); + + it('revert not match given parameters', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value*2, minValidAfter, maxValidBefore, nonce, v, r, s), + 'EIP3009: invalid signature', + ); + }); + + it('revert reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s), + 'EIP3009: authorization is used or canceled', + ); + }); + + it('revert reused nonce', async function () { + let data = buildData(this.chainId, this.token.address); + let signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + await this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s); + + const validAfter = await time.latest(); + data = buildData(this.chainId, this.token.address, validAfter); + signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s), + 'EIP3009: authorization is used or canceled', + ); + }); + + it('revert other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s), + 'EIP3009: invalid signature', + ); + }); + + it('revert not yet valid', async function () { + const validAfter = (await time.latest()) + time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, validAfter); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, validAfter, maxValidBefore, nonce, v, r, s), + 'EIP3009: authorization is not yet valid', + ); + }); + + it('revert aurhorization is expired', async function () { + const validBefore = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, validBefore); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, minValidAfter, validBefore, nonce, v, r, s), + 'EIP3009: authorization is expired', + ); + }); + + it('revert when paused', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.pause({from: pauser}); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s), + 'Pausable: paused', + ); + }); + + it('revert when owner or spender is blocklisted', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.blocklist(from, {from: blocklister}); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s), + 'Blocklistable: account is blocklisted', + ); + + await this.token.unBlocklist(from, {from: blocklister}); + await this.token.blocklist(to, {from: blocklister}); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s), + 'Blocklistable: account is blocklisted', + ); + }); + }); + + describe('ReceiveWithAuthorization', async function () { + const wallet = Wallet.generate(); + let owner = wallet.getAddressString(); + const from = toChecksumAddress(owner); + + beforeEach(async function () { + await this.token.transfer(from, initialSupply, {from: initialHolder}); + }) + + const value = new BN(42); + const minValidAfter = new BN(0); + const maxValidBefore = MAX_UINT256; + const nonce = "0x" + crypto.randomBytes(32).toString('hex'); + + const buildData = (chainId, verifyingContract, validAfter = minValidAfter, validBefore = maxValidBefore) => ({ + primaryType: 'ReceiveWithAuthorization', + types: { EIP712Domain, ReceiveWithAuthorization }, + domain: { name, version, chainId, verifyingContract }, + message: { from, to, value, validAfter, validBefore, nonce }, + }); + + it('accepts owner signature and caller is the payee', async function () { + expect(await this.token.balanceOf(from)).to.be.bignumber.equal(initialSupply); + expect(await this.token.balanceOf(to)).to.be.bignumber.equal(new BN(0)); + + expect(await this.token.authorizationState(from, nonce)).to.equal(false); + + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}); + + expect(await this.token.balanceOf(from)).to.be.bignumber.equal(initialSupply.sub(value)); + expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + + expect(await this.token.authorizationState(from, nonce)).to.equal(true); + }); + + it('emits a AuthorizationUsed event', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const result = await this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}); + + expectEvent(result, 'AuthorizationUsed', { + authorizer: from, + nonce: nonce + }); + }); + + it('reverts tha caller is not the payee', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: initialHolder}), + 'EIP3009: caller must be the payee', + ); + }); + + it('revert not match given parameters', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value*2, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}), + 'EIP3009: invalid signature', + ); + }); + + it('revert reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}), + 'EIP3009: authorization is used or canceled', + ); + }); + + it('revert reused nonce', async function () { + let data = buildData(this.chainId, this.token.address); + let signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + await this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}); + + const validAfter = await time.latest(); + data = buildData(this.chainId, this.token.address, validAfter); + signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}), + 'EIP3009: authorization is used or canceled', + ); + }); + + it('revert other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}), + 'EIP3009: invalid signature', + ); + }); + + it('revert not yet valid', async function () { + const validAfter = (await time.latest()) + time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, validAfter); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, validAfter, maxValidBefore, nonce, v, r, s, {from: to}), + 'EIP3009: authorization is not yet valid', + ); + }); + + it('revert aurhorization is expired', async function () { + const validBefore = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, validBefore); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, minValidAfter, validBefore, nonce, v, r, s, {from: to}), + 'EIP3009: authorization is expired', + ); + }); + + it('revert when paused', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.pause({from: pauser}); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}), + 'Pausable: paused', + ); + }); + + it('revert when owner or spender is blocklisted', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.blocklist(from, {from: blocklister}); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}), + 'Blocklistable: account is blocklisted', + ); + + await this.token.unBlocklist(from, {from: blocklister}); + await this.token.blocklist(to, {from: blocklister}); + + await expectRevert( + this.token.receiveWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s, {from: to}), + 'Blocklistable: account is blocklisted', + ); + }); + }); + + describe('CancelWithAuthorization', async function () { + const wallet = Wallet.generate(); + let owner = wallet.getAddressString(); + const from = toChecksumAddress(owner); + const authorizer = from; + + beforeEach(async function () { + await this.token.transfer(from, initialSupply, {from: initialHolder}); + }) + + const value = new BN(42); + const minValidAfter = new BN(0); + const maxValidBefore = MAX_UINT256; + const nonce = "0x" + crypto.randomBytes(32).toString('hex'); + + const buildData = (chainId, verifyingContract, validAfter = minValidAfter, validBefore = maxValidBefore) => ({ + primaryType: 'TransferWithAuthorization', + types: { EIP712Domain, TransferWithAuthorization }, + domain: { name, version, chainId, verifyingContract }, + message: { from, to, value, validAfter, validBefore, nonce }, + }); + + const buildData2 = (chainId, verifyingContract) => ({ + primaryType: 'CancelAuthorization', + types: { EIP712Domain, CancelAuthorization }, + domain: { name, version, chainId, verifyingContract }, + message: { authorizer, nonce }, + }); + + it('cancel unused transfer authorization and owner signature', async function () { + expect(await this.token.authorizationState(from, nonce)).to.equal(false); + + let data = buildData(this.chainId, this.token.address); + let signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + const transfer_v =v; + const transfer_r =r; + const transfer_s =s; + + data = buildData2(this.chainId, this.token.address); + signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + await this.token.cancelAuthorization(authorizer, nonce, v, r, s); + + expect(await this.token.authorizationState(from, nonce)).to.equal(true); + + await expectRevert( + this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, transfer_v, transfer_r, transfer_s), + 'EIP3009: authorization is used or canceled', + ); + }); + + it('emits a AuthorizationCanceled event', async function () { + let data = buildData(this.chainId, this.token.address); + let signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + data = buildData2(this.chainId, this.token.address); + signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + const result = await this.token.cancelAuthorization(authorizer, nonce, v, r, s); + + expectEvent(result, 'AuthorizationCanceled', { + authorizer: authorizer, + nonce: nonce + }); + }); + + it('revert other signature', async function () { + let data = buildData(this.chainId, this.token.address); + let signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + const transfer_v =v; + const transfer_r =r; + const transfer_s =s; + + expect(await this.token.authorizationState(authorizer, nonce)).to.equal(false); + + const otherWallet = Wallet.generate(); + data = buildData2(this.chainId, this.token.address); + signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.cancelAuthorization(authorizer, nonce, v, r, s), + 'EIP3009: invalid signature', + ); + + expect(await this.token.authorizationState(authorizer, nonce)).to.equal(false); + + await this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, transfer_v, transfer_r, transfer_s); + }); + + it('reverts authorization has already been used', async function () { + let data = buildData(this.chainId, this.token.address); + let signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + await this.token.transferWithAuthorization(from, to, value, minValidAfter, maxValidBefore, nonce, v, r, s); + + data = buildData(this.chainId, this.token.address); + signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + var { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.cancelAuthorization(authorizer, nonce, v, r, s), + 'EIP3009: authorization is used or canceled', + ); + }); + + it('reverts authorization has already benn canceled', async function () { + const data = buildData2(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.cancelAuthorization(authorizer, nonce, v, r, s); + + await expectRevert( + this.token.cancelAuthorization(authorizer, nonce, v, r, s), + 'EIP3009: authorization is used or canceled', + ); + }); + + it('revert when paused', async function () { + const data = buildData2(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.pause({from: pauser}); + + await expectRevert( + this.token.cancelAuthorization(authorizer, nonce, v, r, s), + 'Pausable: paused', + ); + }); + }); +} + +module.exports = { + shouldBehaveLikeEIP3009, +} \ No newline at end of file diff --git a/test/v1_1/EIP712Domain.behavior.js b/test/v1_1/EIP712Domain.behavior.js new file mode 100644 index 000000000..4dcdac7e5 --- /dev/null +++ b/test/v1_1/EIP712Domain.behavior.js @@ -0,0 +1,27 @@ +const { expect } = require('chai') + +const { domainSeparator } = require('../helpers/eip712') + +function shouldBehaveLikeEIP712Domain( + errorPrefix, + name, + version="1" +) { + beforeEach(function () { + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = 1337 // hardhat.confing.js + }) + + it('expected proxiableUUID _IMPLEMENTATION_SLOT', async function () { + const expectedDomainSeparator = await domainSeparator(name, version, this.chainId, this.token.address); + expect( + await this.token.DOMAIN_SEPARATOR() + ).to.equal(expectedDomainSeparator); + }) +} + +module.exports = { + shouldBehaveLikeEIP712Domain, +} diff --git a/test/v1_1/FiatTokenV1_1.test.js b/test/v1_1/FiatTokenV1_1.test.js new file mode 100644 index 000000000..574f64c68 --- /dev/null +++ b/test/v1_1/FiatTokenV1_1.test.js @@ -0,0 +1,232 @@ +const { + BN, + constants, + expectEvent, + expectRevert, +} = require('@openzeppelin/test-helpers') +const { expect } = require('chai') +const { ZERO_ADDRESS } = constants + +const { shouldBehaveLikeERC20 } = require('../v1/ERC20.behavior') + +const { shouldBehaveLikeBlocklistable } = require('../v1/Blocklistable.behavior') + +const { shouldBehaveLikeOwnable } = require('../v1/Ownable.behabvior') + +const { shouldBehaveLikePausable } = require('../v1/Pausable.behavior') + +const { shouldBehaveLikeRescuable } = require('../v1/Rescuable.behavior') + +const { shouldBehaveLikeFiatTokenV1 } = require('../v1/FiatTokenV1.behavior') + +const { shouldBehaveLikeUUPSUpgradeable } = require('../v1/UUPSUpgradeable.behavior') + +const { shouldBehaveLikeEIP712Domain } = require('./EIP712Domain.behavior') + +const { shouldBehaveLikeEIP2612 } = require(`./EIP2612.behavior`) + +const { shouldBehaveLikeEIP3009 } = require(`./EIP3009.behavior`) + +const { artifacts } = require('hardhat') + +const FiatTokenV1_1 = artifacts.require('FiatTokenV1_1') +const FiatTokenV1_1Test = artifacts.require('FiatTokenV1_1Test') + +contract('FiatTokenV1_1', function (accounts) { + const initialHolder = accounts[0] + const recipient = accounts[1] + const anotherAccount = accounts[2] + + const name = 'JPY Coin' + const symbol = 'JPYC' + const currency = 'JPY' + const decimals = 18 + const minterAdmin = accounts[3] + const pauser = accounts[4] + const blocklister = accounts[5] + const owner = initialHolder + + const minter = accounts[6] + const blocklisted = accounts[7] + const unblocklisted = accounts[8] + const rescuer = accounts[9] + + const initialSupply = new BN(100) + + beforeEach(async function () { + this.token = await FiatTokenV1_1.new() + await this.token.initialize( + name, + symbol, + currency, + decimals, + minterAdmin, + pauser, + blocklister, + rescuer, + owner + ) + await this.token.configureMinter(minter, initialSupply, { + from: minterAdmin, + }) + await this.token.mint(initialHolder, initialSupply, { from: minter }) + await this.token.blocklist(blocklisted, { from: blocklister }) + }) + + it('already initialized', async function () { + await expectRevert( + this.token.initialize( + name, + symbol, + currency, + decimals, + minterAdmin, + pauser, + blocklister, + rescuer, + owner + ), + 'FiatToken: contract is already initialized' + ) + }) + + it('has a name', async function () { + expect(await this.token.name()).to.equal(name) + }) + + it('has a symbol', async function () { + expect(await this.token.symbol()).to.equal(symbol) + }) + + it('has a currency', async function () { + expect(await this.token.symbol()).to.equal(symbol) + }) + + it('has 18 decimals', async function () { + expect(await this.token.decimals()).to.be.bignumber.equal('18') + }) + + it('has a minterAdmin', async function () { + expect(await this.token.minterAdmin()).to.equal(minterAdmin) + }) + + it('has a pauser', async function () { + expect(await this.token.pauser()).to.equal(pauser) + }) + + it('has a blockLister', async function () { + expect(await this.token.blocklister()).to.equal(blocklister) + }) + + it('has an owner', async function () { + expect(await this.token.owner()).to.equal(owner) + }) + + it('has a rescuer', async function () { + expect(await this.token.rescuer()).to.equal(rescuer) + }) + + it('has a version', async function () { + expect(await this.token.version()).to.be.bignumber.equal('1') + }) + + // 直接呼び出せないのでFiatTokenV1を継承したmockを使用 + it('_approve test', async function () { + const tokenTest = await FiatTokenV1_1Test.new() + const value = new BN(100) + await expectRevert( + tokenTest.approveTest(ZERO_ADDRESS, recipient, value), + 'FiatToken: approve from the zero address' + ) + }) + + describe('shouldBehaveLikeERC20', () => { + shouldBehaveLikeERC20( + 'FiatToken', + initialSupply, + initialHolder, + recipient, + anotherAccount + ) + }) + + describe('shouldBehaveLikeBlocklistable', () => { + shouldBehaveLikeBlocklistable( + 'Blocklistable', + blocklister, + blocklisted, + unblocklisted, + owner, + initialSupply, + initialHolder, + anotherAccount, + minterAdmin + ) + }) + + describe('shouldBehaveLikeOwnable', () => { + shouldBehaveLikeOwnable('Ownable', owner, anotherAccount) + }) + + describe('shouldBehaveLikePausable', () => { + shouldBehaveLikePausable( + 'Pausable', + pauser, + owner, + anotherAccount, + initialSupply, + initialHolder, + recipient, + minterAdmin + ) + }) + + describe('shouldBehaveLikeRescuable', () => { + shouldBehaveLikeRescuable('Rescuable', rescuer, owner, anotherAccount) + }) + + describe('shouldBehaveLikeFiatTokenV1', () => { + shouldBehaveLikeFiatTokenV1( + 'FiatToken', + minterAdmin, + anotherAccount, + recipient, + initialSupply, + owner + ) + }) + + describe('shouldBehaveLikeUUPSUpgradeable', () => { + shouldBehaveLikeUUPSUpgradeable('UUPSUPgradable') + }) + + describe('shouldBehaveLikeEIP2612', () => { + shouldBehaveLikeEIP2612( + 'EIP2612', + name, + initialHolder, + recipient, + pauser, + blocklister + ) + }) + + describe('shouldBehaveLikeEIP3009', () => { + shouldBehaveLikeEIP3009( + 'EIP3009', + name, + initialSupply, + initialHolder, + recipient, + pauser, + blocklister + ) + }) + + describe('shouldBehaveLikeEIP712Domain', () => { + shouldBehaveLikeEIP712Domain( + 'EIP712Domain', + name + ) + }) +}) diff --git a/test/v1_1_proxy/FiatTokenV1_1_proxy.test.js b/test/v1_1_proxy/FiatTokenV1_1_proxy.test.js new file mode 100644 index 000000000..0d1c3258a --- /dev/null +++ b/test/v1_1_proxy/FiatTokenV1_1_proxy.test.js @@ -0,0 +1,239 @@ +const { + BN, + constants, + expectEvent, + expectRevert, +} = require('@openzeppelin/test-helpers') +const { expect } = require('chai') +const { ZERO_ADDRESS } = constants +const { _data } = require('../helpers/DataMaker') + +const { shouldBehaveLikeERC20 } = require('../v1/ERC20.behavior') + +const { shouldBehaveLikeBlocklistable } = require('../v1/Blocklistable.behavior') + +const { shouldBehaveLikeOwnable } = require('../v1/Ownable.behabvior') + +const { shouldBehaveLikePausable } = require('../v1/Pausable.behavior') + +const { shouldBehaveLikeRescuable } = require('../v1/Rescuable.behavior') + +const { shouldBehaveLikeFiatTokenV1 } = require('../v1/FiatTokenV1.behavior') + +const { shouldBehaveLikeUUPSUpgradeable } = require('../v1/UUPSUpgradeable.behavior') + +const { shouldBehaveLikeEIP712Domain } = require('../v1_1/EIP712Domain.behavior') + +const { shouldBehaveLikeEIP2612 } = require(`../v1_1/EIP2612.behavior`) + +const { shouldBehaveLikeEIP3009 } = require(`../v1_1/EIP3009.behavior`) + +const { artifacts } = require('hardhat') + +const FiatTokenV1 = artifacts.require('FiatTokenV1') +const FiatTokenV1_1 = artifacts.require('FiatTokenV1_1') +const FiatTokenV1_1Test = artifacts.require('FiatTokenV1_1Test') +const ERC1967Proxy = artifacts.require('ERC1967Proxy') + +contract('FiatTokenV1_1_proxy', function (accounts) { + const initialHolder = accounts[0] + const recipient = accounts[1] + const anotherAccount = accounts[2] + + const name = 'JPY Coin' + const symbol = 'JPYC' + const currency = 'JPY' + const decimals = 18 + const minterAdmin = accounts[3] + const pauser = accounts[4] + const blocklister = accounts[5] + const owner = initialHolder + + const minter = accounts[6] + const blocklisted = accounts[7] + const unblocklisted = accounts[8] + const rescuer = accounts[9] + + const initialSupply = new BN(100) + + beforeEach(async function () { + this.implementationV1 = await FiatTokenV1.new() + this.implementationV1_1 = await FiatTokenV1_1.new() + + // initialize implementation v1 in proxy + this.proxyContract = await ERC1967Proxy.new( + this.implementationV1.address, + _data(minterAdmin, pauser, blocklister, rescuer, owner) + ) + // call tokenImple from proxy address + this.tokenImpleAtProxy = await FiatTokenV1.at(this.proxyContract.address) + + // additional case setting + await this.tokenImpleAtProxy.configureMinter(minter, initialSupply, { + from: minterAdmin, + }) + await this.tokenImpleAtProxy.mint(initialHolder, initialSupply, { + from: minter, + }) + await this.tokenImpleAtProxy.blocklist(blocklisted, { from: blocklister }) + + // upgrade to v1_1 + this.tokenImpleAtProxy.upgradeTo(this.implementationV1_1.address, { from: owner }) + + // Call v1_1 implementation from proxy + this.token = await FiatTokenV1_1.at(this.proxyContract.address) + }) + + it('already initialized', async function () { + await expectRevert( + this.token.initialize( + name, + symbol, + currency, + decimals, + minterAdmin, + pauser, + blocklister, + rescuer, + owner + ), + 'FiatToken: contract is already initialized' + ) + }) + + it('has a name', async function () { + expect(await this.token.name()).to.equal(name) + }) + + it('has a symbol', async function () { + expect(await this.token.symbol()).to.equal(symbol) + }) + + it('has a currency', async function () { + expect(await this.token.symbol()).to.equal(symbol) + }) + + it('has 18 decimals', async function () { + expect(await this.token.decimals()).to.be.bignumber.equal('18') + }) + + it('has a minterAdmin', async function () { + expect(await this.token.minterAdmin()).to.equal(minterAdmin) + }) + + it('has a pauser', async function () { + expect(await this.token.pauser()).to.equal(pauser) + }) + + it('has a blockLister', async function () { + expect(await this.token.blocklister()).to.equal(blocklister) + }) + + it('has an owner', async function () { + expect(await this.token.owner()).to.equal(owner) + }) + + it('has a rescuer', async function () { + expect(await this.token.rescuer()).to.equal(rescuer) + }) + + it('has a version', async function () { + expect(await this.token.version()).to.be.bignumber.equal('1') + }) + + // 直接呼び出せないのでFiatTokenV1を継承したmockを使用 + it('_approve test', async function () { + const tokenTest = await FiatTokenV1_1Test.new() + const value = new BN(100) + await expectRevert( + tokenTest.approveTest(ZERO_ADDRESS, recipient, value), + 'FiatToken: approve from the zero address' + ) + }) + + describe('shouldBehaveLikeERC20', () => { + shouldBehaveLikeERC20( + 'FiatToken', + initialSupply, + initialHolder, + recipient, + anotherAccount + ) + }) + + describe('shouldBehaveLikeBlocklistable', () => { + shouldBehaveLikeBlocklistable( + 'Blocklistable', + blocklister, + blocklisted, + unblocklisted, + owner, + initialSupply, + initialHolder, + anotherAccount, + minterAdmin + ) + }) + + describe('shouldBehaveLikeOwnable', () => { + shouldBehaveLikeOwnable('Ownable', owner, anotherAccount) + }) + + describe('shouldBehaveLikePausable', () => { + shouldBehaveLikePausable( + 'Pausable', + pauser, + owner, + anotherAccount, + initialSupply, + initialHolder, + recipient, + minterAdmin + ) + }) + + describe('shouldBehaveLikeRescuable', () => { + shouldBehaveLikeRescuable('Rescuable', rescuer, owner, anotherAccount) + }) + + describe('shouldBehaveLikeFiatTokenV1', () => { + shouldBehaveLikeFiatTokenV1( + 'FiatToken', + minterAdmin, + anotherAccount, + recipient, + initialSupply, + owner + ) + }) + + describe('shouldBehaveLikeEIP2612', () => { + shouldBehaveLikeEIP2612( + 'EIP2612', + name, + initialHolder, + recipient, + pauser, + blocklister + ) + }) + + describe('shouldBehaveLikeEIP3009', () => { + shouldBehaveLikeEIP3009( + 'EIP3009', + name, + initialSupply, + initialHolder, + recipient, + pauser, + blocklister + ) + }) + + describe('shouldBehaveLikeEIP712Domain', () => { + shouldBehaveLikeEIP712Domain( + 'EIP712Domain', + name + ) + }) +}) From 2fbccd29e974fe5eb10dab0138e2a60bb66f6307 Mon Sep 17 00:00:00 2001 From: retocrooman Date: Wed, 11 Jan 2023 17:12:40 +0900 Subject: [PATCH 3/4] add version function and rename details --- contracts/v1_1/EIP712Domain.sol | 25 +++++++++++++++------ contracts/v1_1/FiatTokenV1_1.sol | 14 ++++++------ test/README.md | 1 + test/v1_1/EIP712Domain.behavior.js | 6 ++++- test/v1_1/FiatTokenV1_1.test.js | 4 ---- test/v1_1_proxy/FiatTokenV1_1_proxy.test.js | 4 ---- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/contracts/v1_1/EIP712Domain.sol b/contracts/v1_1/EIP712Domain.sol index ec8167f75..b808b2af1 100644 --- a/contracts/v1_1/EIP712Domain.sol +++ b/contracts/v1_1/EIP712Domain.sol @@ -44,25 +44,36 @@ contract EIP712Domain { /** * @dev EIP712 Domain Separator */ - bytes32 internal _CACHED_DOMAIN_SEPARATOR; - uint256 internal _CACHED_CHAIN_ID; - string internal _CACHED_NAME; - string internal _CACHED_VERSION; + bytes32 internal CACHED_DOMAIN_SEPARATOR; + uint256 internal CACHED_CHAIN_ID; + string internal CACHED_NAME; + string internal CACHED_VERSION; /** * @dev Returns the domain separator for the current chain. */ function _domainSeparatorV4() internal view returns (bytes32) { - if(block.chainid == _CACHED_CHAIN_ID) { - return _CACHED_DOMAIN_SEPARATOR; + if(block.chainid == CACHED_CHAIN_ID) { + return CACHED_DOMAIN_SEPARATOR; } else { - return EIP712.makeDomainSeparator(_CACHED_NAME, _CACHED_VERSION); + return EIP712.makeDomainSeparator(CACHED_NAME, CACHED_VERSION); } } + /** + * @dev EIP712 Domain Separator + */ function DOMAIN_SEPARATOR() external view returns(bytes32) { return _domainSeparatorV4(); } + /** + * @notice Version string for the EIP712 domain separator + * @return Version string + */ + function version() external view returns (string memory) { + return CACHED_VERSION; + } + uint256[50] private __gap; } \ No newline at end of file diff --git a/contracts/v1_1/FiatTokenV1_1.sol b/contracts/v1_1/FiatTokenV1_1.sol index d3609e62a..87476e075 100644 --- a/contracts/v1_1/FiatTokenV1_1.sol +++ b/contracts/v1_1/FiatTokenV1_1.sol @@ -68,7 +68,7 @@ contract FiatTokenV1_1 is uint256 internal totalSupply_; address public minterAdmin; uint8 public decimals; - uint8 public version; + uint8 internal initializedVersion; mapping(address => uint256) internal balances; mapping(address => mapping(address => uint256)) internal allowed; @@ -93,7 +93,7 @@ contract FiatTokenV1_1 is address newOwner ) public { require( - version == 0, + initializedVersion == 0, "FiatToken: contract is already initialized" ); require( @@ -127,11 +127,11 @@ contract FiatTokenV1_1 is rescuer = newRescuer; _transferOwnership(newOwner); blocklisted[address(this)] = 1; - _CACHED_DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(tokenName, "1"); - _CACHED_CHAIN_ID = block.chainid; - _CACHED_NAME = tokenName; - _CACHED_VERSION = "1"; - version = 1; + CACHED_DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(tokenName, "1"); + CACHED_CHAIN_ID = block.chainid; + CACHED_NAME = tokenName; + CACHED_VERSION = "1"; + initializedVersion = 1; } /** diff --git a/test/README.md b/test/README.md index 62951f209..fac8b14bc 100644 --- a/test/README.md +++ b/test/README.md @@ -398,6 +398,7 @@ - domain separator(v1/EIP3009.behavior.js) - add list below - DOMAIN_SEPARATOR(v1_1/EIP712Domain.behavior.js) + - version(v1_1/EIP712Domain.behavior.js) ## v1_1 proxy - all test of v1_1 except the list below diff --git a/test/v1_1/EIP712Domain.behavior.js b/test/v1_1/EIP712Domain.behavior.js index 4dcdac7e5..2cd67077a 100644 --- a/test/v1_1/EIP712Domain.behavior.js +++ b/test/v1_1/EIP712Domain.behavior.js @@ -14,12 +14,16 @@ function shouldBehaveLikeEIP712Domain( this.chainId = 1337 // hardhat.confing.js }) - it('expected proxiableUUID _IMPLEMENTATION_SLOT', async function () { + it('has a DOMAIN_SEPARATOR', async function () { const expectedDomainSeparator = await domainSeparator(name, version, this.chainId, this.token.address); expect( await this.token.DOMAIN_SEPARATOR() ).to.equal(expectedDomainSeparator); }) + + it('has a version', async function () { + expect(await this.token.version()).to.equal(version) + }) } module.exports = { diff --git a/test/v1_1/FiatTokenV1_1.test.js b/test/v1_1/FiatTokenV1_1.test.js index 574f64c68..93438c70b 100644 --- a/test/v1_1/FiatTokenV1_1.test.js +++ b/test/v1_1/FiatTokenV1_1.test.js @@ -126,10 +126,6 @@ contract('FiatTokenV1_1', function (accounts) { expect(await this.token.rescuer()).to.equal(rescuer) }) - it('has a version', async function () { - expect(await this.token.version()).to.be.bignumber.equal('1') - }) - // 直接呼び出せないのでFiatTokenV1を継承したmockを使用 it('_approve test', async function () { const tokenTest = await FiatTokenV1_1Test.new() diff --git a/test/v1_1_proxy/FiatTokenV1_1_proxy.test.js b/test/v1_1_proxy/FiatTokenV1_1_proxy.test.js index 0d1c3258a..0e868b2d9 100644 --- a/test/v1_1_proxy/FiatTokenV1_1_proxy.test.js +++ b/test/v1_1_proxy/FiatTokenV1_1_proxy.test.js @@ -137,10 +137,6 @@ contract('FiatTokenV1_1_proxy', function (accounts) { expect(await this.token.rescuer()).to.equal(rescuer) }) - it('has a version', async function () { - expect(await this.token.version()).to.be.bignumber.equal('1') - }) - // 直接呼び出せないのでFiatTokenV1を継承したmockを使用 it('_approve test', async function () { const tokenTest = await FiatTokenV1_1Test.new() From 6dfc15777e7d317c8e43d1f80d84783efa423e50 Mon Sep 17 00:00:00 2001 From: retocrooman Date: Wed, 11 Jan 2023 17:40:32 +0900 Subject: [PATCH 4/4] fix FiatTokenV1Test name --- contracts/test/{FiatTokenV1Test copy.sol => FiatTokenV1Test.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/test/{FiatTokenV1Test copy.sol => FiatTokenV1Test.sol} (100%) diff --git a/contracts/test/FiatTokenV1Test copy.sol b/contracts/test/FiatTokenV1Test.sol similarity index 100% rename from contracts/test/FiatTokenV1Test copy.sol rename to contracts/test/FiatTokenV1Test.sol