Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BT-235: ERC2612 - Add ERC-20 Permit function compatibility #236

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions contracts/token/Token.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@

pragma solidity 0.8.27;

import "./IToken.sol";
import "@onchain-id/solidity/contracts/interface/IIdentity.sol";
import "./TokenStorage.sol";
import "../roles/AgentRoleUpgradeable.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "../roles/IERC173.sol";
import "../errors/InvalidArgumentErrors.sol";
import "./IToken.sol";
import "./TokenPermit.sol";
import "./TokenStorage.sol";
import "../errors/CommonErrors.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "../errors/InvalidArgumentErrors.sol";
import "../roles/AgentRoleUpgradeable.sol";

/// errors

Expand Down Expand Up @@ -119,7 +120,13 @@ error DefaultAllowanceAlreadyDisabled(address _user);
error DefaultAllowanceAlreadySet(address _target);


contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165 {
contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPermit {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are implementing ERC165, therefore we should add IERC20Permit to the supported interfaces


bytes32 private constant _TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

bytes32 private constant _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

/// modifiers

Expand Down Expand Up @@ -172,6 +179,7 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165 {
, EmptyString());
require(0 <= _decimals && _decimals <= 18, DecimalsOutOfRange(_decimals));
__Ownable_init();
__TokenPermit_init(_name);
_tokenName = _name;
_tokenSymbol = _symbol;
_tokenDecimals = _decimals;
Expand Down Expand Up @@ -728,7 +736,7 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165 {
address _owner,
address _spender,
uint256 _amount
) internal virtual {
) internal virtual override {
require(_owner != address(0), ERC20InvalidSender(_owner));
require(_spender != address(0), ERC20InvalidSpender(_spender));

Expand Down
121 changes: 121 additions & 0 deletions contracts/token/TokenPermit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: GPL-3.0
//
// :+#####%%%%%%%%%%%%%%+
// .-*@@@%+.:+%@@@@@%%#***%@@%=
// :=*%@@@#=. :#@@% *@@@%=
// .-+*%@%*-.:+%@@@@@@+. -*+: .=#. :%@@@%-
// :=*@@@@%%@@@@@@@@@%@@@- .=#@@@%@%= =@@@@#.
// -=+#%@@%#*=:. :%@@@@%. -*@@#*@@@@@@@#=:- *@@@@+
// =@@%=:. :=: *@@@@@%#- =%*%@@@@#+-. =+ :%@@@%-
// -@@%. .+@@@ =+=-. @@#- +@@@%- =@@@@%:
// :@@@. .+@@#%: : .=*=-::.-%@@@+*@@= +@@@@#.
// %@@: +@%%* =%@@@@@@@@@@@#. .*@%- +@@@@*.
// #@@= .+@@@@%:=*@@@@@- :%@%: .*@@@@+
// *@@* +@@@#-@@%-:%@@* +@@#. :%@@@@-
// -@@% .:-=++*##%%%@@@@@@@@@@@@*. :@+.@@@%: .#@@+ =@@@@#:
// .@@@*-+*#%%%@@@@@@@@@@@@@@@@%%#**@@%@@@. *@=*@@# :#@%= .#@@@@#-
// -%@@@@@@@@@@@@@@@*+==-:-@@@= *@# .#@*-=*@@@@%= -%@@@* =@@@@@%-
// -+%@@@#. %@%%= -@@:+@: -@@* *@@*-:: -%@@%=. .*@@@@@#
// *@@@* +@* *@@##@@- #@*@@+ -@@= . :+@@@#: .-+@@@%+-
// +@@@%*@@:..=@@@@* .@@@* .#@#. .=+- .=%@@@*. :+#@@@@*=:
// =@@@@%@@@@@@@@@@@@@@@@@@@@@@%- :+#*. :*@@@%=. .=#@@@@%+:
// .%@@= ..... .=#@@+. .#@@@*: -*%@@@@%+.
// +@@#+===---:::... .=%@@*- +@@@+. -*@@@@@%+.
// -@@@@@@@@@@@@@@@@@@@@@@%@@@@= -@@@+ -#@@@@@#=.
// ..:::---===+++***###%%%@@@#- .#@@+ -*@@@@@#=.
// @@@@@@+. +@@*. .+@@@@@%=.
// -@@@@@= =@@%: -#@@@@%+.
// +@@@@@. =@@@= .+@@@@@*:
// #@@@@#:%@@#. :*@@@@#-
// @@@@@%@@@= :#@@@@+.
// :@@@@@@@#.:#@@@%-
// +@@@@@@-.*@@@*:
// #@@@@#.=@@@+.
// @@@@+-%@%=
// :@@@#%@%=
// +@@@@%-
// :#%%=
//

/**
* NOTICE
*
* The T-REX software is licensed under a proprietary license or the GPL v.3.
* If you choose to receive it under the GPL v.3 license, the following applies:
* T-REX is a suite of smart contracts implementing the ERC-3643 standard and
* developed by Tokeny to manage and transfer financial assets on EVM blockchains
*
* Copyright (C) 2023, Tokeny sàrl.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

pragma solidity 0.8.27;

import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Named imports are preferred when possible

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that as been done for token-factory, but in trex all import are "classical"... But ok to update it.

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../utils/EIP712Upgradeable.sol";
import "../utils/NoncesUpgradeable.sol";

error ERC2612ExpiredSignature(uint256 deadline);
error ERC2612InvalidSigner(address signer, address owner);


abstract contract TokenPermit is IERC20Permit, EIP712Upgradeable, NoncesUpgradeable {

bytes32 private constant _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

/// @inheritdoc IERC20Permit
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(block.timestamp <= deadline, ERC2612ExpiredSignature(deadline));

bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));

bytes32 hash = _hashTypedDataV4(structHash);

address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, ERC2612InvalidSigner(signer, owner));

_approve(owner, spender, value);
}

/// @inheritdoc IERC20Permit
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _domainSeparatorV4();
}

/// @inheritdoc IERC20Permit
function nonces(address owner) public view override(IERC20Permit, NoncesUpgradeable) returns (uint256) {
return super.nonces(owner);
}

// solhint-disable-next-line func-name-mixedcase
function __TokenPermit_init(string memory name) internal {
__EIP712_init_unchained(name, "1");
}

/// @dev Implemented in Token.sol
function _approve(address owner, address spender, uint256 value) internal virtual;

}
217 changes: 217 additions & 0 deletions contracts/utils/EIP712Upgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/EIP712.sol)

pragma solidity 0.8.27;

import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { IERC5267 } from "@openzeppelin/contracts/interfaces/IERC5267.sol";


/**
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP-712] is a standard for hashing and signing of typed structured data.
*
* The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose
* encoding is very generic and therefore its implementation in Solidity is not feasible, thus this contract
* does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in order to
* produce the hash of their typed data using a combination of `abi.encode` and `keccak256`.
*
* This contract implements the EIP-712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
* scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
* ({_hashTypedDataV4}).
*
* The implementation of the domain separator was designed to be as efficient as possible while still properly updating
* the chain id to protect against replay attacks on an eventual fork of the chain.
*
* NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
* https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
*
* NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain
* separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the
* separator from the immutable values, which is cheaper than accessing a cached version in cold storage.
*/
abstract contract EIP712Upgradeable is IERC5267 {

/// @custom:storage-location erc7201:openzeppelin.storage.EIP712
struct EIP712Storage {
/// @custom:oz-renamed-from _HASHED_NAME
bytes32 _hashedName;
/// @custom:oz-renamed-from _HASHED_VERSION
bytes32 _hashedVersion;

string _name;
string _version;
}

bytes32 private constant _TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.EIP712")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant _EIP712_STORAGE_LOCATION = 0xa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100;

/**
* @dev See {IERC-5267}.
*/
function eip712Domain()
public
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why this function is public and not external? It seems to be external in the specification and I haven't seen it being explicitly called internally within the contract

https://eips.ethereum.org/EIPS/eip-5267

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view
virtual
returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
)
{
EIP712Storage storage $ = _getEIP712Storage();
// If the hashed name and version in storage are non-zero, the contract hasn't been properly initialized
// and the EIP712 domain is not reliable, as it will be missing name and version.
require($._hashedName == 0 && $._hashedVersion == 0, "EIP712: Uninitialized");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom error instead of string could help to save some gas here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact I have just copy/paste OpenZeppelin contract to be sure to not add regression (just reorder function for solhint). At first I try to just import the file, but I haven't succeed to manage several version into the package.json. I need the last version of contract-upgradeable (for the custom storage), but it have dependencies on last OpenZeppelin contracts which is not the one we are using...
So either we find a way to just import the correct version, if not we can maybe replace with a custom error.


return (
hex"0f", // 01111
_EIP712Name(),
_EIP712Version(),
block.chainid,
address(this),
bytes32(0),
new uint256[](0)
);
}

/**
* @dev Initializes the domain separator and parameter caches.
*
* The meaning of `name` and `version` is specified in
* https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP-712]:
*
* - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
* - `version`: the current major version of the signing domain.
*
* NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
* contract upgrade].
*/
// solhint-disable-next-line func-name-mixedcase
function __EIP712_init(string memory name, string memory version) internal {
__EIP712_init_unchained(name, version);
}

// solhint-disable-next-line func-name-mixedcase
function __EIP712_init_unchained(string memory name, string memory version) internal {
EIP712Storage storage $ = _getEIP712Storage();
$._name = name;
$._version = version;

// Reset prior values in storage if upgrading
$._hashedName = 0;
$._hashedVersion = 0;
}

/**
* @dev The name parameter for the EIP712 domain.
*
* NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs
* are a concern.
*/
// solhint-disable-next-line func-name-mixedcase
function _EIP712Name() internal view virtual returns (string memory) {
EIP712Storage storage $ = _getEIP712Storage();
return $._name;
}

/**
* @dev The version parameter for the EIP712 domain.
*
* NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs
* are a concern.
*/
// solhint-disable-next-line func-name-mixedcase
function _EIP712Version() internal view virtual returns (string memory) {
EIP712Storage storage $ = _getEIP712Storage();
return $._version;
}

/**
* @dev The hash of the name parameter for the EIP712 domain.
*
* NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Name` instead.
*/
// solhint-disable-next-line func-name-mixedcase
function _EIP712NameHash() internal view returns (bytes32) {
EIP712Storage storage $ = _getEIP712Storage();
string memory name = _EIP712Name();
if (bytes(name).length > 0) {
return keccak256(bytes(name));
} else {
// If the name is empty, the contract may have been upgraded without initializing the new storage.
// We return the name hash in storage if non-zero, otherwise we assume the name is empty by design.
bytes32 hashedName = $._hashedName;
if (hashedName != 0) {
return hashedName;
} else {
return keccak256("");
}
}
}

/**
* @dev The hash of the version parameter for the EIP712 domain.
*
* NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Version` instead.
*/
// solhint-disable-next-line func-name-mixedcase
function _EIP712VersionHash() internal view returns (bytes32) {
EIP712Storage storage $ = _getEIP712Storage();
string memory version = _EIP712Version();
if (bytes(version).length > 0) {
return keccak256(bytes(version));
} else {
// If the version is empty, the contract may have been upgraded without initializing the new storage.
// We return the version hash in storage if non-zero, otherwise we assume the version is empty by design.
bytes32 hashedVersion = $._hashedVersion;
if (hashedVersion != 0) {
return hashedVersion;
} else {
return keccak256("");
}
}
}

/**
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
* function returns the hash of the fully encoded EIP712 message for this domain.
*
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
*
* ```solidity
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
* keccak256("Mail(address to,string contents)"),
* mailTo,
* keccak256(bytes(mailContents))
* )));
* address signer = ECDSA.recover(digest, signature);
* ```
*/
function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash);
}

/**
* @dev Returns the domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
return _buildDomainSeparator();
}

function _buildDomainSeparator() private view returns (bytes32) {
return keccak256(abi.encode(_TYPE_HASH, _EIP712NameHash(), _EIP712VersionHash(), block.chainid, address(this)));
}

function _getEIP712Storage() private pure returns (EIP712Storage storage $) {
assembly {
$.slot := _EIP712_STORAGE_LOCATION
}
}
}
Loading
Loading