Skip to content

Commit

Permalink
add permit to non-rebasable token
Browse files Browse the repository at this point in the history
  • Loading branch information
kovalgek committed Apr 9, 2024
1 parent 0b22b0f commit f563fab
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 85 deletions.
35 changes: 35 additions & 0 deletions contracts/token/ERC20BridgedPermit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido <info@lido.fi>
// 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_);
}
}
107 changes: 107 additions & 0 deletions contracts/token/ERC20Permit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido <info@lido.fi>
// 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();
}
24 changes: 22 additions & 2 deletions contracts/token/ERC20Rebasable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)) {
Expand All @@ -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();
Expand Down
92 changes: 9 additions & 83 deletions contracts/token/ERC20RebasablePermit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
6 changes: 6 additions & 0 deletions test/token/ERC20Rebasable.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit f563fab

Please sign in to comment.