Skip to content

Commit

Permalink
Merge pull request #74 from lidofinance/feat/steth-permit
Browse files Browse the repository at this point in the history
Add ERC-2612/EIP-1271 permit to rebasable token
  • Loading branch information
kovalgek authored Apr 2, 2024
2 parents 18a1dc5 + ba0b41e commit 624219c
Show file tree
Hide file tree
Showing 7 changed files with 739 additions and 1 deletion.
77 changes: 77 additions & 0 deletions contracts/lib/SignatureChecker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0
// Writen based on (utils/cryptography/SignatureChecker.sol from d398d68

pragma solidity 0.8.10;

import {ECDSA} from "@openzeppelin/contracts-v4.9/utils/cryptography/ECDSA.sol";
import {IERC1271} from "@openzeppelin/contracts-v4.9/interfaces/IERC1271.sol";



/**
* @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA
* signatures from externally owned accounts (EOAs) as well as ERC-1271 signatures from smart contract wallets like
* Argent and Safe Wallet (previously Gnosis Safe).
*/
library SignatureChecker {
/**
* @dev Checks if a signature is valid for a given signer and data hash. If the signer is a smart contract, the
* signature is validated against that smart contract using ERC-1271, otherwise it's validated using `ECDSA.recover`.
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
if (signer.code.length == 0) {
// return true;
(address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(hash, signature);
return err == ECDSA.RecoverError.NoError && recovered == signer;
} else {
return isValidERC1271SignatureNow(signer, hash, signature);
}
}

/**
* @dev Checks signature validity.
*
* If the signer address doesn't contain any code, assumes that the address is externally owned
* and the signature is a ECDSA signature generated using its private key. Otherwise, issues a
* static call to the signer address to check the signature validity using the ERC-1271 standard.
*/
function isValidSignatureNow(
address signer,
bytes32 msgHash,
uint8 v,
bytes32 r,
bytes32 s
) internal view returns (bool) {
if (signer.code.length == 0) {
(address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(msgHash, v, r, s);
return err == ECDSA.RecoverError.NoError && recovered == signer;
} else {
bytes memory signature = abi.encodePacked(r, s, v);
return isValidERC1271SignatureNow(signer, msgHash, signature);
}
}

/**
* @dev Checks if a signature is valid for a given signer and data hash. The signature is validated
* against the signer smart contract using ERC-1271.
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidERC1271SignatureNow(
address signer,
bytes32 hash,
bytes memory signature
) internal view returns (bool) {
(bool success, bytes memory result) = signer.staticcall(
abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature)
);
return (success &&
result.length >= 32 &&
abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector));
}
}
21 changes: 21 additions & 0 deletions contracts/stubs/ERC1271PermitSignerMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2024 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.10;


contract ERC1271PermitSignerMock {
bytes4 public constant ERC1271_MAGIC_VALUE = 0x1626ba7e;

function sign(bytes32 hash) public view returns (bytes1 v, bytes32 r, bytes32 s) {
v = 0x42;
r = hash;
s = bytes32(bytes20(address(this)));
}

function isValidSignature(bytes32 hash, bytes memory sig) external view returns (bytes4) {
(bytes1 v, bytes32 r, bytes32 s) = sign(hash);
bytes memory validSig = abi.encodePacked(r, s, v);
return keccak256(sig) == keccak256(validSig) ? ERC1271_MAGIC_VALUE : bytes4(0);
}
}
113 changes: 113 additions & 0 deletions contracts/token/ERC20RebasablePermit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

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";


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;

/// @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 wrappedToken_ address of the ERC20 token to wrap
/// @param tokenRateOracle_ address of oracle that returns tokens rate
/// @param bridge_ The bridge address which allowd to mint/burn tokens
constructor(
string memory name_,
string memory symbol_,
string memory version_,
uint8 decimals_,
address wrappedToken_,
address tokenRateOracle_,
address bridge_
)
ERC20Rebasable(name_, symbol_, decimals_, wrappedToken_, tokenRateOracle_, bridge_)
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();
}
_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();
}

/**
* @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();
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-promise": "^5.2.0",
"ethereum-waffle": "^3.4.4",
"ethereumjs-util": "^7.0.8",
"ethers": "^5.6.2",
"hardhat": "^2.12.2",
"hardhat-gas-reporter": "^1.0.8",
Expand All @@ -74,6 +75,7 @@
"@ethersproject/providers": "^5.6.8",
"@lidofinance/evm-script-decoder": "^0.2.2",
"@openzeppelin/contracts": "4.6.0",
"@openzeppelin/contracts-v4.9": "npm:@openzeppelin/[email protected]",
"chalk": "4.1.2"
}
}
Loading

0 comments on commit 624219c

Please sign in to comment.