From f563fab9045c620d6fbbefbbb9ffef6b51d19b9e Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 9 Apr 2024 23:02:58 +0200 Subject: [PATCH] add permit to non-rebasable token --- contracts/token/ERC20BridgedPermit.sol | 35 ++++++++ contracts/token/ERC20Permit.sol | 107 +++++++++++++++++++++++ contracts/token/ERC20Rebasable.sol | 24 ++++- contracts/token/ERC20RebasablePermit.sol | 92 ++----------------- test/token/ERC20Rebasable.unit.test.ts | 6 ++ 5 files changed, 179 insertions(+), 85 deletions(-) create mode 100644 contracts/token/ERC20BridgedPermit.sol create mode 100644 contracts/token/ERC20Permit.sol diff --git a/contracts/token/ERC20BridgedPermit.sol b/contracts/token/ERC20BridgedPermit.sol new file mode 100644 index 0000000..d936ae3 --- /dev/null +++ b/contracts/token/ERC20BridgedPermit.sol @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ERC20Bridged} from "./ERC20Bridged.sol"; +import {ERC20Permit} from "./ERC20Permit.sol"; + +contract ERC20BridgedPermit is ERC20Bridged, ERC20Permit { + + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + /// @param version_ The current major version of the signing domain (aka token version) + /// @param decimals_ The decimals places of the token + /// @param bridge_ The bridge address which allowd to mint/burn tokens + constructor( + string memory name_, + string memory symbol_, + string memory version_, + uint8 decimals_, + address bridge_ + ) + ERC20Bridged(name_, symbol_, decimals_, bridge_) + ERC20Permit(name_, version_) + { + } + + function _permitAccepted( + address owner_, + address spender_, + uint256 amount_ + ) internal override { + _approve(owner_, spender_, amount_); + } +} diff --git a/contracts/token/ERC20Permit.sol b/contracts/token/ERC20Permit.sol new file mode 100644 index 0000000..634731c --- /dev/null +++ b/contracts/token/ERC20Permit.sol @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {UnstructuredStorage} from "./UnstructuredStorage.sol"; +import {EIP712} from "@openzeppelin/contracts-v4.9/utils/cryptography/EIP712.sol"; +import {IERC2612} from "@openzeppelin/contracts-v4.9/interfaces/IERC2612.sol"; +import {SignatureChecker} from "../lib/SignatureChecker.sol"; + +contract ERC20Permit is IERC2612, EIP712 { + using UnstructuredStorage for bytes32; + + /** + * @dev Nonces for ERC-2612 (Permit) + */ + mapping(address => uint256) internal noncesByAddress; + + // TODO: outline structured storage used because at least EIP712 uses it + + /** + * @dev Typehash constant for ERC-2612 (Permit) + * + * keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + */ + bytes32 internal constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + /// @param name_ The name of the token + /// @param version_ The current major version of the signing domain (aka token version) + constructor( + string memory name_, + string memory version_ + ) EIP712(name_, version_) + { + } + + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + */ + function permit( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s + ) external { + if (block.timestamp > _deadline) { + revert ErrorDeadlineExpired(); + } + + bytes32 structHash = keccak256( + abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, _useNonce(_owner), _deadline) + ); + + bytes32 hash = _hashTypedDataV4(structHash); + + if (!SignatureChecker.isValidSignatureNow(_owner, hash, _v, _r, _s)) { + revert ErrorInvalidSignature(); + } + + _permitAccepted(_owner, _spender, _value); + } + + function _permitAccepted( + address owner_, + address spender_, + uint256 amount_ + ) internal virtual { + } + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256) { + return noncesByAddress[owner]; + } + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + */ + function _useNonce(address _owner) internal returns (uint256 current) { + current = noncesByAddress[_owner]; + noncesByAddress[_owner] = current + 1; + } + + error ErrorInvalidSignature(); + error ErrorDeadlineExpired(); +} diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index eb7f875..4993954 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -191,7 +191,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta ) internal onlyNonZeroAccount(from_) onlyNonZeroAccount(to_) { uint256 sharesToTransfer = _getSharesByTokens(amount_); _transferShares(from_, to_, sharesToTransfer); - emit Transfer(from_, to_, amount_); + _emitTransferEvents(from_, to_, amount_ ,sharesToTransfer); } /// @dev Updates owner_'s allowance for spender_ based on spent amount_. Does not update @@ -271,7 +271,8 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta ) internal onlyNonZeroAccount(recipient_) { _setTotalShares(_getTotalShares() + amount_); _getShares()[recipient_] = _getShares()[recipient_] + amount_; - emit Transfer(address(0), recipient_, amount_); + uint256 tokensAmount = _getTokensByShares(amount_); + _emitTransferEvents(address(0), recipient_, tokensAmount ,amount_); } /// @dev Destroys amount_ shares from account_, reducing the total shares supply. @@ -307,6 +308,17 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta _getShares()[recipient_] = _getShares()[recipient_] + sharesAmount_; } + /// @dev Emits `Transfer` and `TransferShares` events + function _emitTransferEvents( + address _from, + address _to, + uint _tokenAmount, + uint256 _sharesAmount + ) internal { + emit Transfer(_from, _to, _tokenAmount); + emit TransferShares(_from, _to, _sharesAmount); + } + /// @dev validates that account_ is not zero address modifier onlyNonZeroAccount(address account_) { if (account_ == address(0)) { @@ -323,6 +335,14 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta _; } + /// @notice An executed shares transfer from `sender` to `recipient`. + /// @dev emitted in pair with an ERC20-defined `Transfer` event. + event TransferShares( + address indexed from, + address indexed to, + uint256 sharesValue + ); + error ErrorZeroSharesWrap(); error ErrorZeroTokensUnwrap(); error ErrorTokenRateDecimalsIsZero(); diff --git a/contracts/token/ERC20RebasablePermit.sol b/contracts/token/ERC20RebasablePermit.sol index 22353a0..5760644 100644 --- a/contracts/token/ERC20RebasablePermit.sol +++ b/contracts/token/ERC20RebasablePermit.sol @@ -3,30 +3,10 @@ pragma solidity 0.8.10; -import {UnstructuredStorage} from "./UnstructuredStorage.sol"; import {ERC20Rebasable} from "./ERC20Rebasable.sol"; -import {EIP712} from "@openzeppelin/contracts-v4.9/utils/cryptography/EIP712.sol"; -import {IERC2612} from "@openzeppelin/contracts-v4.9/interfaces/IERC2612.sol"; -import {SignatureChecker} from "../lib/SignatureChecker.sol"; +import {ERC20Permit} from "./ERC20Permit.sol"; - -contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { - using UnstructuredStorage for bytes32; - - /** - * @dev Nonces for ERC-2612 (Permit) - */ - mapping(address => uint256) internal noncesByAddress; - - // TODO: outline structured storage used because at least EIP712 uses it - - /** - * @dev Typehash constant for ERC-2612 (Permit) - * - * keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") - */ - bytes32 internal constant PERMIT_TYPEHASH = - 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; +contract ERC20RebasablePermit is ERC20Rebasable, ERC20Permit { /// @param name_ The name of the token /// @param symbol_ The symbol of the token @@ -45,69 +25,15 @@ contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { address bridge_ ) ERC20Rebasable(name_, symbol_, decimals_, wrappedToken_, tokenRateOracle_, bridge_) - EIP712(name_, version_) + ERC20Permit(name_, version_) { } - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - */ - function permit( - address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s - ) external { - if (block.timestamp > _deadline) { - revert ErrorDeadlineExpired(); - } - - bytes32 structHash = keccak256( - abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, _useNonce(_owner), _deadline) - ); - - bytes32 hash = _hashTypedDataV4(structHash); - - if (!SignatureChecker.isValidSignatureNow(_owner, hash, _v, _r, _s)) { - revert ErrorInvalidSignature(); - } - _approve(_owner, _spender, _value); - } - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256) { - return noncesByAddress[owner]; - } - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparatorV4(); + function _permitAccepted( + address owner_, + address spender_, + uint256 amount_ + ) internal override { + _approve(owner_, spender_, amount_); } - - /** - * @dev "Consume a nonce": return the current value and increment. - */ - function _useNonce(address _owner) internal returns (uint256 current) { - current = noncesByAddress[_owner]; - noncesByAddress[_owner] = current + 1; - } - - error ErrorInvalidSignature(); - error ErrorDeadlineExpired(); } diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index e763ec0..84f7c22 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -737,7 +737,13 @@ unit("ERC20Rebasable", ctxFactory) .bridgeMintShares(recipient.address, mintAmount); // validate Transfer event was emitted + const mintAmountInTokens = await rebasableProxied.getTokensByShares(mintAmount); await assert.emits(rebasableProxied, tx, "Transfer", [ + hre.ethers.constants.AddressZero, + recipient.address, + mintAmountInTokens, + ]); + await assert.emits(rebasableProxied, tx, "TransferShares", [ hre.ethers.constants.AddressZero, recipient.address, mintAmount,