-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #74 from lidofinance/feat/steth-permit
Add ERC-2612/EIP-1271 permit to rebasable token
- Loading branch information
Showing
7 changed files
with
739 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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" | ||
} | ||
} |
Oops, something went wrong.