From 261216a54f5bb65d922b621935dd7c3e448b081e Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 11:10:13 +0200 Subject: [PATCH 01/13] feat(eip712): add eip712 base contract --- src/EIP712.sol | 69 ++++++++++++++++++++++++++++++++++++++ src/interfaces/IEIP712.sol | 33 ++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/EIP712.sol create mode 100644 src/interfaces/IEIP712.sol diff --git a/src/EIP712.sol b/src/EIP712.sol new file mode 100644 index 0000000..790513e --- /dev/null +++ b/src/EIP712.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IEIP712} from "src/interfaces/IEIP712.sol"; + +/// @dev The prefix used for EIP-712 signature. +string constant EIP712_MSG_PREFIX = "\x19\x01"; + +/// @dev The domain typehash used for the EIP-712 signature. +bytes32 constant EIP712_DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + +/// @dev The highest valid value for s in an ECDSA signature pair (0 < s < secp256k1n ÷ 2 + 1). +uint256 constant MAX_VALID_ECDSA_S = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0; + +/// @notice EIP712 helpers. +/// @dev Maintains cross-chain replay protection in the event of a fork. +/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol +contract EIP712 is IEIP712 { + /// @dev The reference chainid. Used to check whether the chain forked and offer replay protection. + uint256 private immutable _CACHED_CHAIN_ID; + + /// @dev The cached domain separator to use if chainid didnt change. + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; + + /// @dev The name used for EIP-712 signature. + bytes32 private immutable _NAMEHASH; + + constructor(string memory name) { + _NAMEHASH = keccak256(abi.encodePacked(name)); + + _CACHED_CHAIN_ID = block.chainid; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(EIP712_DOMAIN_TYPEHASH, _NAMEHASH); + } + + /// @notice Returns the domain separator for the current chain. + /// @dev Uses cached version if chainid and address are unchanged from construction. + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return block.chainid == _CACHED_CHAIN_ID + ? _CACHED_DOMAIN_SEPARATOR + : _buildDomainSeparator(EIP712_DOMAIN_TYPEHASH, _NAMEHASH); + } + + /* INTERNAL */ + + function _verify(Signature calldata signature, bytes32 dataHash, uint256 deadline, address signer) internal view { + if (block.timestamp >= deadline) revert SignatureExpired(); + if (uint256(signature.s) > MAX_VALID_ECDSA_S) revert InvalidValueS(); + // v ∈ {27, 28} (source: https://ethereum.github.io/yellowpaper/paper.pdf #308) + if (signature.v != 27 && signature.v != 28) revert InvalidValueV(); + + bytes32 digest = _hashTypedData(dataHash); + address recovered = ecrecover(digest, signature.v, signature.r, signature.s); + + if (recovered == address(0) || signer != recovered) revert InvalidSignature(); + } + + /* PRIVATE */ + + /// @notice Builds a domain separator using the current chainId and contract address. + function _buildDomainSeparator(bytes32 typeHash, bytes32 nameHash) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, block.chainid, address(this))); + } + + /// @notice Creates an EIP-712 typed data hash + function _hashTypedData(bytes32 dataHash) private view returns (bytes32) { + return keccak256(abi.encodePacked(EIP712_MSG_PREFIX, DOMAIN_SEPARATOR(), dataHash)); + } +} diff --git a/src/interfaces/IEIP712.sol b/src/interfaces/IEIP712.sol new file mode 100644 index 0000000..acf4ffc --- /dev/null +++ b/src/interfaces/IEIP712.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +interface IEIP712 { + /* STRUCTS */ + + struct Signature { + uint8 v; + bytes32 r; + bytes32 s; + } + + /* ERRORS */ + + /// @notice Thrown when the s part of the ECDSA signature is invalid. + error InvalidValueS(); + + /// @notice Thrown when the v part of the ECDSA signature is invalid. + error InvalidValueV(); + + /// @notice Thrown when the signer of the ECDSA signature is invalid. + error InvalidSignature(); + + /// @notice Thrown when the nonce is invalid. + error InvalidNonce(); + + /// @notice Thrown when the signature deadline is expired. + error SignatureExpired(); + + /* FUNCTIONS */ + + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From ed7192dbc16f5a34b6b04f0936602118e846c2b3 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 11:27:15 +0200 Subject: [PATCH 02/13] feat(ownable): add ownable helpers --- src/access/Ownable.sol | 70 +++++++++++++++++++++++++ src/access/Ownable2Step.sol | 55 +++++++++++++++++++ src/interfaces/access/IOwnable.sol | 14 +++++ src/interfaces/access/IOwnable2Step.sol | 15 ++++++ 4 files changed, 154 insertions(+) create mode 100644 src/access/Ownable.sol create mode 100644 src/access/Ownable2Step.sol create mode 100644 src/interfaces/access/IOwnable.sol create mode 100644 src/interfaces/access/IOwnable2Step.sol diff --git a/src/access/Ownable.sol b/src/access/Ownable.sol new file mode 100644 index 0000000..01bd4e2 --- /dev/null +++ b/src/access/Ownable.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IOwnable} from "src/interfaces/access/IOwnable.sol"; + +/// @notice Gas-optimized Ownable helpers. +/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol +abstract contract Ownable is IOwnable { + address private _owner; + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor(address initialOwner) { + _transferOwnership(initialOwner); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + + _; + } + + /* PUBLIC */ + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + * + * NOTE: Ownership can be renounced by transferring ownership to `address(0)`. + * Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + _transferOwnership(newOwner); + } + + /* INTERNAL */ + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + address currentOwner = owner(); + + if (currentOwner != msg.sender) revert OwnershipRequired(currentOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = owner(); + + _owner = newOwner; + + emit OwnershipTransferred(oldOwner, newOwner); + } +} diff --git a/src/access/Ownable2Step.sol b/src/access/Ownable2Step.sol new file mode 100644 index 0000000..ec37395 --- /dev/null +++ b/src/access/Ownable2Step.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IOwnable2Step} from "src/interfaces/access/IOwnable2Step.sol"; + +import {Ownable} from "src/access/Ownable.sol"; + +/// @notice Gas-optimized Ownable2Step helpers. +/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable2Step.sol +abstract contract Ownable2Step is IOwnable2Step, Ownable { + address private _pendingOwner; + + /* PUBLIC */ + + /** + * @dev Returns the address of the pending owner. + */ + function pendingOwner() public view virtual returns (address) { + return _pendingOwner; + } + + /** + * @dev The new owner accepts the ownership transfer. + */ + function acceptOwnership() public virtual { + address sender = msg.sender; + + address pending = pendingOwner(); + if (pending != sender) revert PendingOwnershipRequired(pending); + + _transferOwnership(sender); + } + + /** + * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one. + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual override onlyOwner { + _pendingOwner = newOwner; + + emit OwnershipTransferStarted(owner(), newOwner); + } + + /* INTERNAL */ + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner. + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual override { + delete _pendingOwner; + + super._transferOwnership(newOwner); + } +} diff --git a/src/interfaces/access/IOwnable.sol b/src/interfaces/access/IOwnable.sol new file mode 100644 index 0000000..3fcccf1 --- /dev/null +++ b/src/interfaces/access/IOwnable.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +interface IOwnable { + /* EVENTS */ + + /// @dev Emitted when owner changes from `previousOwner` to `newOwner`. + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /* ERRORS */ + + /// @dev Thrown when ownership is required to perform an action. + error OwnershipRequired(address owner); +} diff --git a/src/interfaces/access/IOwnable2Step.sol b/src/interfaces/access/IOwnable2Step.sol new file mode 100644 index 0000000..f377302 --- /dev/null +++ b/src/interfaces/access/IOwnable2Step.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IOwnable} from "src/interfaces/access/IOwnable.sol"; + +interface IOwnable2Step is IOwnable { + /* EVENTS */ + + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + + /* ERRORS */ + + /// @dev Thrown when pending ownership is required to accept ownership. + error PendingOwnershipRequired(address pendingOwner); +} From 400caba9a2a782ce466b7ff90ff7a6df06561a9b Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 11:54:33 +0200 Subject: [PATCH 03/13] test(ownable): added Ownable tests --- src/EIP712.sol | 3 +- src/access/Ownable.sol | 32 +++---------- src/access/Ownable2Step.sol | 25 ++++------- src/interfaces/IEIP712.sol | 2 + src/interfaces/access/IOwnable.sol | 10 +++++ src/interfaces/access/IOwnable2Step.sol | 13 ++++++ test/access/TestOwnable.sol | 41 +++++++++++++++++ test/access/TestOwnable2Step.sol | 60 +++++++++++++++++++++++++ test/mocks/Ownable2StepMock.sol | 8 ++++ test/mocks/OwnableMock.sol | 8 ++++ 10 files changed, 159 insertions(+), 43 deletions(-) create mode 100644 test/access/TestOwnable.sol create mode 100644 test/access/TestOwnable2Step.sol create mode 100644 test/mocks/Ownable2StepMock.sol create mode 100644 test/mocks/OwnableMock.sol diff --git a/src/EIP712.sol b/src/EIP712.sol index 790513e..9e4950f 100644 --- a/src/EIP712.sol +++ b/src/EIP712.sol @@ -33,8 +33,7 @@ contract EIP712 is IEIP712 { _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(EIP712_DOMAIN_TYPEHASH, _NAMEHASH); } - /// @notice Returns the domain separator for the current chain. - /// @dev Uses cached version if chainid and address are unchanged from construction. + /// @inheritdoc IEIP712 function DOMAIN_SEPARATOR() public view returns (bytes32) { return block.chainid == _CACHED_CHAIN_ID ? _CACHED_DOMAIN_SEPARATOR diff --git a/src/access/Ownable.sol b/src/access/Ownable.sol index 01bd4e2..ca64540 100644 --- a/src/access/Ownable.sol +++ b/src/access/Ownable.sol @@ -5,19 +5,15 @@ import {IOwnable} from "src/interfaces/access/IOwnable.sol"; /// @notice Gas-optimized Ownable helpers. /// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol -abstract contract Ownable is IOwnable { +contract Ownable is IOwnable { address private _owner; - /** - * @dev Initializes the contract setting the deployer as the initial owner. - */ + /// @dev Initializes the contract setting the deployer as the initial owner. constructor(address initialOwner) { _transferOwnership(initialOwner); } - /** - * @dev Throws if called by any account other than the owner. - */ + /// @dev Throws if called by any account other than the owner. modifier onlyOwner() { _checkOwner(); @@ -26,40 +22,26 @@ abstract contract Ownable is IOwnable { /* PUBLIC */ - /** - * @dev Returns the address of the current owner. - */ + /// @inheritdoc IOwnable function owner() public view virtual returns (address) { return _owner; } - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Can only be called by the current owner. - * - * NOTE: Ownership can be renounced by transferring ownership to `address(0)`. - * Renouncing ownership will leave the contract without an owner, - * thereby removing any functionality that is only available to the owner. - */ + /// @inheritdoc IOwnable function transferOwnership(address newOwner) public virtual onlyOwner { _transferOwnership(newOwner); } /* INTERNAL */ - /** - * @dev Throws if the sender is not the owner. - */ + /// @dev Throws if the sender is not the owner. function _checkOwner() internal view virtual { address currentOwner = owner(); if (currentOwner != msg.sender) revert OwnershipRequired(currentOwner); } - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Internal function without access restriction. - */ + /// @dev Transfers ownership of the contract to a new account (`newOwner`). Internal function without access restriction. function _transferOwnership(address newOwner) internal virtual { address oldOwner = owner(); diff --git a/src/access/Ownable2Step.sol b/src/access/Ownable2Step.sol index ec37395..99bebe8 100644 --- a/src/access/Ownable2Step.sol +++ b/src/access/Ownable2Step.sol @@ -7,21 +7,20 @@ import {Ownable} from "src/access/Ownable.sol"; /// @notice Gas-optimized Ownable2Step helpers. /// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable2Step.sol -abstract contract Ownable2Step is IOwnable2Step, Ownable { +contract Ownable2Step is IOwnable2Step, Ownable { address private _pendingOwner; + /// @dev Initializes the contract setting the deployer as the initial owner. + constructor(address initialOwner) Ownable(initialOwner) {} + /* PUBLIC */ - /** - * @dev Returns the address of the pending owner. - */ + /// @inheritdoc IOwnable2Step function pendingOwner() public view virtual returns (address) { return _pendingOwner; } - /** - * @dev The new owner accepts the ownership transfer. - */ + /// @inheritdoc IOwnable2Step function acceptOwnership() public virtual { address sender = msg.sender; @@ -31,11 +30,8 @@ abstract contract Ownable2Step is IOwnable2Step, Ownable { _transferOwnership(sender); } - /** - * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one. - * Can only be called by the current owner. - */ - function transferOwnership(address newOwner) public virtual override onlyOwner { + /// @inheritdoc IOwnable2Step + function transferOwnership(address newOwner) public virtual override(IOwnable2Step, Ownable) onlyOwner { _pendingOwner = newOwner; emit OwnershipTransferStarted(owner(), newOwner); @@ -43,10 +39,7 @@ abstract contract Ownable2Step is IOwnable2Step, Ownable { /* INTERNAL */ - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner. - * Internal function without access restriction. - */ + /// @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner. Internal function without access restriction. function _transferOwnership(address newOwner) internal virtual override { delete _pendingOwner; diff --git a/src/interfaces/IEIP712.sol b/src/interfaces/IEIP712.sol index acf4ffc..e802f61 100644 --- a/src/interfaces/IEIP712.sol +++ b/src/interfaces/IEIP712.sol @@ -29,5 +29,7 @@ interface IEIP712 { /* FUNCTIONS */ + /// @notice Returns the domain separator for the current chain. + /// @dev Uses cached version if chainid and address are unchanged from construction. function DOMAIN_SEPARATOR() external view returns (bytes32); } diff --git a/src/interfaces/access/IOwnable.sol b/src/interfaces/access/IOwnable.sol index 3fcccf1..61bca1a 100644 --- a/src/interfaces/access/IOwnable.sol +++ b/src/interfaces/access/IOwnable.sol @@ -11,4 +11,14 @@ interface IOwnable { /// @dev Thrown when ownership is required to perform an action. error OwnershipRequired(address owner); + + /* FUNCTIONS */ + + /// @dev Returns the address of the current owner. + function owner() external view returns (address); + + /// @notice Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner. + /// @dev Ownership can be renounced by transferring ownership to `address(0)`. + /// Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner. + function transferOwnership(address newOwner) external; } diff --git a/src/interfaces/access/IOwnable2Step.sol b/src/interfaces/access/IOwnable2Step.sol index f377302..3770155 100644 --- a/src/interfaces/access/IOwnable2Step.sol +++ b/src/interfaces/access/IOwnable2Step.sol @@ -6,10 +6,23 @@ import {IOwnable} from "src/interfaces/access/IOwnable.sol"; interface IOwnable2Step is IOwnable { /* EVENTS */ + /// @dev Emitted when ownership transfer process is initiated. event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); /* ERRORS */ /// @dev Thrown when pending ownership is required to accept ownership. error PendingOwnershipRequired(address pendingOwner); + + /* FUNCTIONS */ + + /// @dev Returns the address of the pending owner. + function pendingOwner() external view returns (address); + + /// @dev The new owner accepts the ownership transfer. + function acceptOwnership() external; + + /// @dev Starts the ownership transfer of the contract to a new account. + /// Replaces the pending transfer if there is one. Can only be called by the current owner. + function transferOwnership(address newOwner) external; } diff --git a/test/access/TestOwnable.sol b/test/access/TestOwnable.sol new file mode 100644 index 0000000..9051ae2 --- /dev/null +++ b/test/access/TestOwnable.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IOwnable} from "src/interfaces/access/IOwnable.sol"; + +import {OwnableMock} from "test/mocks/OwnableMock.sol"; + +import "forge-std/Test.sol"; + +contract TestOwnable is Test { + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + IOwnable mock; + + function setUp() public { + mock = new OwnableMock(address(this)); + } + + function testConstructorOwner(address initialOwner) public { + mock = new OwnableMock(initialOwner); + + assertEq(mock.owner(), initialOwner); + } + + function testTransferOwnership(address newOwner) public { + vm.expectEmit(true, true, true, true, address(mock)); + emit OwnershipTransferred(address(this), newOwner); + + mock.transferOwnership(newOwner); + + assertEq(mock.owner(), newOwner); + } + + function testTransferOwnershipOnlyOwner(address pranked) public { + vm.assume(pranked != address(this)); + + vm.prank(pranked); + vm.expectRevert(abi.encodeWithSelector(IOwnable.OwnershipRequired.selector, address(this))); + mock.transferOwnership(pranked); + } +} diff --git a/test/access/TestOwnable2Step.sol b/test/access/TestOwnable2Step.sol new file mode 100644 index 0000000..4f275a4 --- /dev/null +++ b/test/access/TestOwnable2Step.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IOwnable} from "src/interfaces/access/IOwnable.sol"; +import {IOwnable2Step} from "src/interfaces/access/IOwnable2Step.sol"; + +import {Ownable2StepMock} from "test/mocks/Ownable2StepMock.sol"; + +import "forge-std/Test.sol"; + +contract TestOwnable2Step is Test { + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + + IOwnable2Step mock; + + function setUp() public { + mock = new Ownable2StepMock(address(this)); + } + + function testConstructorOwner(address initialOwner) public { + mock = new Ownable2StepMock(initialOwner); + + assertEq(mock.owner(), initialOwner); + } + + function testTransferOwnership(address newOwner) public { + vm.expectEmit(true, true, true, true, address(mock)); + emit OwnershipTransferStarted(address(this), newOwner); + + mock.transferOwnership(newOwner); + + assertEq(mock.owner(), address(this), "owner != address(this)"); + assertEq(mock.pendingOwner(), newOwner, "pendingOwner != newOwner"); + + vm.prank(newOwner); + mock.acceptOwnership(); + + assertEq(mock.owner(), newOwner, "owner != newOwner"); + assertEq(mock.pendingOwner(), address(0), "pendingOwner != address(0)"); + } + + function testTransferOwnershipOnlyOwner(address pranked) public { + vm.assume(pranked != address(this)); + + vm.prank(pranked); + vm.expectRevert(abi.encodeWithSelector(IOwnable.OwnershipRequired.selector, address(this))); + mock.transferOwnership(pranked); + } + + function testAcceptOwnershipOnlyOwner(address pendingOwner, address pranked) public { + vm.assume(pranked != pendingOwner); + + mock.transferOwnership(pendingOwner); + + vm.prank(pranked); + vm.expectRevert(abi.encodeWithSelector(IOwnable2Step.PendingOwnershipRequired.selector, pendingOwner)); + mock.acceptOwnership(); + } +} diff --git a/test/mocks/Ownable2StepMock.sol b/test/mocks/Ownable2StepMock.sol new file mode 100644 index 0000000..3c336c0 --- /dev/null +++ b/test/mocks/Ownable2StepMock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {Ownable2Step, Ownable} from "src/access/Ownable2Step.sol"; + +contract Ownable2StepMock is Ownable2Step { + constructor(address initialOwner) Ownable2Step(initialOwner) {} +} diff --git a/test/mocks/OwnableMock.sol b/test/mocks/OwnableMock.sol new file mode 100644 index 0000000..830cee9 --- /dev/null +++ b/test/mocks/OwnableMock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {Ownable} from "src/access/Ownable.sol"; + +contract OwnableMock is Ownable { + constructor(address initialOwner) Ownable(initialOwner) {} +} From 5c96ca44e2b3c166228c866183f8e077a59c1fde Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 12:39:54 +0200 Subject: [PATCH 04/13] test(eip712): added eip712 tests --- src/EIP712.sol | 52 +++++++++++++++++----- src/interfaces/IEIP712.sol | 12 +++++- test/TestEIP712.sol | 88 ++++++++++++++++++++++++++++++++++++++ test/mocks/EIP712Mock.sol | 18 ++++++++ 4 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 test/TestEIP712.sol create mode 100644 test/mocks/EIP712Mock.sol diff --git a/src/EIP712.sol b/src/EIP712.sol index 9e4950f..638e409 100644 --- a/src/EIP712.sol +++ b/src/EIP712.sol @@ -26,39 +26,69 @@ contract EIP712 is IEIP712 { /// @dev The name used for EIP-712 signature. bytes32 private immutable _NAMEHASH; + /// @dev The nonce used inside by signers to offer signature replay protection. + mapping(address => uint256) private _nonces; + constructor(string memory name) { - _NAMEHASH = keccak256(abi.encodePacked(name)); + _NAMEHASH = keccak256(bytes(name)); _CACHED_CHAIN_ID = block.chainid; - _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(EIP712_DOMAIN_TYPEHASH, _NAMEHASH); + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(); + } + + /* PUBLIC */ + + /// @inheritdoc IEIP712 + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return block.chainid == _CACHED_CHAIN_ID ? _CACHED_DOMAIN_SEPARATOR : _buildDomainSeparator(); } /// @inheritdoc IEIP712 - function DOMAIN_SEPARATOR() public view returns (bytes32) { - return block.chainid == _CACHED_CHAIN_ID - ? _CACHED_DOMAIN_SEPARATOR - : _buildDomainSeparator(EIP712_DOMAIN_TYPEHASH, _NAMEHASH); + function nonce(address user) public view virtual returns (uint256) { + return _nonces[user]; } /* INTERNAL */ - function _verify(Signature calldata signature, bytes32 dataHash, uint256 deadline, address signer) internal view { - if (block.timestamp >= deadline) revert SignatureExpired(); + /// @dev Verifies a signature components against the provided data hash, nonce, deadline and signer. + /// @param signature The signature to verify. + /// @param dataHash The EIP712 message hash the signature should correspond to. + /// @param signedNonce The nonce used along with the provided signature. Must not be an end-user input and must be proven to be signed by the signer. + /// @param deadline The signature's maximum valid timestamp. Must not be an end-user input and must be proven to be signed by the signer. + /// @param signer The expected signature's signer. + function _verify( + Signature calldata signature, + bytes32 dataHash, + uint256 signedNonce, + uint256 deadline, + address signer + ) internal virtual { + if (block.timestamp > deadline) revert SignatureExpired(); if (uint256(signature.s) > MAX_VALID_ECDSA_S) revert InvalidValueS(); // v ∈ {27, 28} (source: https://ethereum.github.io/yellowpaper/paper.pdf #308) if (signature.v != 27 && signature.v != 28) revert InvalidValueV(); + uint256 usedNonce = _useNonce(signer); + if (signedNonce != usedNonce) revert InvalidNonce(usedNonce); + bytes32 digest = _hashTypedData(dataHash); address recovered = ecrecover(digest, signature.v, signature.r, signature.s); - if (recovered == address(0) || signer != recovered) revert InvalidSignature(); + if (recovered == address(0) || signer != recovered) revert InvalidSignature(recovered); + } + + /// @dev Increments and returns the nonce that should have been used in the corresponding signature. + function _useNonce(address signer) internal virtual returns (uint256 usedNonce) { + usedNonce = _nonces[signer]++; + + emit NonceUsed(msg.sender, signer, usedNonce); } /* PRIVATE */ /// @notice Builds a domain separator using the current chainId and contract address. - function _buildDomainSeparator(bytes32 typeHash, bytes32 nameHash) private view returns (bytes32) { - return keccak256(abi.encode(typeHash, nameHash, block.chainid, address(this))); + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, _NAMEHASH, block.chainid, address(this))); } /// @notice Creates an EIP-712 typed data hash diff --git a/src/interfaces/IEIP712.sol b/src/interfaces/IEIP712.sol index e802f61..801ba26 100644 --- a/src/interfaces/IEIP712.sol +++ b/src/interfaces/IEIP712.sol @@ -10,6 +10,11 @@ interface IEIP712 { bytes32 s; } + /* EVENTS */ + + /// @dev Emitted when a signer's nonce is incremented. + event NonceUsed(address indexed caller, address indexed signer, uint256 usedNonce); + /* ERRORS */ /// @notice Thrown when the s part of the ECDSA signature is invalid. @@ -19,10 +24,10 @@ interface IEIP712 { error InvalidValueV(); /// @notice Thrown when the signer of the ECDSA signature is invalid. - error InvalidSignature(); + error InvalidSignature(address recovered); /// @notice Thrown when the nonce is invalid. - error InvalidNonce(); + error InvalidNonce(uint256 nonce); /// @notice Thrown when the signature deadline is expired. error SignatureExpired(); @@ -32,4 +37,7 @@ interface IEIP712 { /// @notice Returns the domain separator for the current chain. /// @dev Uses cached version if chainid and address are unchanged from construction. function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice Returns the given signer's nonce to be used in the next EIP712 signature. + function nonce(address signer) external view returns (uint256); } diff --git a/test/TestEIP712.sol b/test/TestEIP712.sol new file mode 100644 index 0000000..bf41458 --- /dev/null +++ b/test/TestEIP712.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IEIP712} from "src/interfaces/IEIP712.sol"; + +import {EIP712_DOMAIN_TYPEHASH, MAX_VALID_ECDSA_S} from "src/EIP712.sol"; +import {EIP712Mock} from "test/mocks/EIP712Mock.sol"; + +import "forge-std/Test.sol"; + +contract TestEIP712 is Test { + string constant NAME = "Test"; + + EIP712Mock mock; + + function setUp() public { + mock = new EIP712Mock(NAME); + } + + function testDomainSeparator() public { + assertEq( + IEIP712(mock).DOMAIN_SEPARATOR(), + keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, keccak256(bytes(NAME)), block.chainid, address(this))) + ); + } + + function testVerify(bytes32 dataHash, uint256 deadline, uint256 privateKey) public { + privateKey = bound(privateKey, 1, type(uint256).max); + deadline = bound(deadline, block.timestamp, type(uint256).max); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, dataHash); + + mock.verify(IEIP712.Signature({r: r, s: s, v: v}), dataHash, 0, deadline, vm.addr(privateKey)); + } + + function testVerifySignatureExpired(bytes32 dataHash, bytes32 r, uint256 s, uint256 deadline, address signer) + public + { + s = bound(s, 0, MAX_VALID_ECDSA_S); + deadline = bound(deadline, 0, block.timestamp - 1); + + vm.expectRevert(IEIP712.SignatureExpired.selector); + mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, 0, deadline, signer); + + vm.expectRevert(IEIP712.SignatureExpired.selector); + mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, 0, deadline, signer); + } + + function testVerifyInvalidValueS(bytes32 dataHash, bytes32 r, uint256 s, address signer) public { + s = bound(s, MAX_VALID_ECDSA_S + 1, type(uint256).max); + + vm.expectRevert(IEIP712.InvalidValueS.selector); + mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, 0, block.timestamp, signer); + + vm.expectRevert(IEIP712.InvalidValueS.selector); + mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, 0, block.timestamp, signer); + } + + function testVerifyInvalidValueV(bytes32 dataHash, bytes32 r, uint256 s, uint8 v, address signer) public { + s = bound(s, 0, MAX_VALID_ECDSA_S); + vm.assume(v != 27 && v != 28); + + vm.expectRevert(IEIP712.InvalidValueV.selector); + mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: v}), dataHash, 0, block.timestamp, signer); + } + + function testVerifyInvalidNonce(bytes32 dataHash, bytes32 r, uint256 s, uint256 nonce, address signer) public { + s = bound(s, 0, MAX_VALID_ECDSA_S); + nonce = bound(nonce, 1, type(uint256).max); + + vm.expectRevert(abi.encodeWithSelector(IEIP712.InvalidNonce.selector, 0)); + mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, nonce, block.timestamp, signer); + + vm.expectRevert(abi.encodeWithSelector(IEIP712.InvalidNonce.selector, 0)); + mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, nonce, block.timestamp, signer); + } + + function testVerifyInvalidSignature(bytes32 dataHash, uint256 s, uint256 deadline, address signer) public { + deadline = bound(deadline, block.timestamp, type(uint256).max); + s = bound(s, 0, MAX_VALID_ECDSA_S); + + vm.expectRevert(abi.encodeWithSelector(IEIP712.InvalidSignature.selector, address(0))); + mock.verify(IEIP712.Signature({r: 0, s: bytes32(s), v: 27}), dataHash, 0, block.timestamp, signer); + + vm.expectRevert(abi.encodeWithSelector(IEIP712.InvalidSignature.selector, address(0))); + mock.verify(IEIP712.Signature({r: 0, s: bytes32(s), v: 28}), dataHash, 0, block.timestamp, signer); + } +} diff --git a/test/mocks/EIP712Mock.sol b/test/mocks/EIP712Mock.sol new file mode 100644 index 0000000..695037a --- /dev/null +++ b/test/mocks/EIP712Mock.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {EIP712} from "src/EIP712.sol"; + +contract EIP712Mock is EIP712 { + constructor(string memory name) EIP712(name) {} + + function verify( + Signature calldata signature, + bytes32 dataHash, + uint256 signedNonce, + uint256 deadline, + address signer + ) external { + _verify(signature, dataHash, signedNonce, deadline, signer); + } +} From f5a807f177227b2240074604628e0dfe4e1a25fc Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 14:31:32 +0200 Subject: [PATCH 05/13] refactor(erc712): rename eip to erc --- src/{EIP712.sol => ERC712.sol} | 24 ++++----- src/interfaces/{IEIP712.sol => IERC712.sol} | 4 +- test/{TestEIP712.sol => TestERC712.sol} | 54 +++++++++---------- test/mocks/{EIP712Mock.sol => ERC712Mock.sol} | 6 +-- 4 files changed, 44 insertions(+), 44 deletions(-) rename src/{EIP712.sol => ERC712.sol} (86%) rename src/interfaces/{IEIP712.sol => IERC712.sol} (96%) rename test/{TestEIP712.sol => TestERC712.sol} (58%) rename test/mocks/{EIP712Mock.sol => ERC712Mock.sol} (72%) diff --git a/src/EIP712.sol b/src/ERC712.sol similarity index 86% rename from src/EIP712.sol rename to src/ERC712.sol index 638e409..c37e082 100644 --- a/src/EIP712.sol +++ b/src/ERC712.sol @@ -1,22 +1,22 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IEIP712} from "src/interfaces/IEIP712.sol"; +import {IERC712} from "src/interfaces/IERC712.sol"; /// @dev The prefix used for EIP-712 signature. -string constant EIP712_MSG_PREFIX = "\x19\x01"; +string constant ERC712_MSG_PREFIX = "\x19\x01"; /// @dev The domain typehash used for the EIP-712 signature. -bytes32 constant EIP712_DOMAIN_TYPEHASH = - keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); +bytes32 constant ERC712_DOMAIN_TYPEHASH = + keccak256("ERC712Domain(string name,uint256 chainId,address verifyingContract)"); /// @dev The highest valid value for s in an ECDSA signature pair (0 < s < secp256k1n ÷ 2 + 1). uint256 constant MAX_VALID_ECDSA_S = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0; -/// @notice EIP712 helpers. +/// @notice ERC712 helpers. /// @dev Maintains cross-chain replay protection in the event of a fork. -/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol -contract EIP712 is IEIP712 { +/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ERC712.sol +contract ERC712 is IERC712 { /// @dev The reference chainid. Used to check whether the chain forked and offer replay protection. uint256 private immutable _CACHED_CHAIN_ID; @@ -38,12 +38,12 @@ contract EIP712 is IEIP712 { /* PUBLIC */ - /// @inheritdoc IEIP712 + /// @inheritdoc IERC712 function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { return block.chainid == _CACHED_CHAIN_ID ? _CACHED_DOMAIN_SEPARATOR : _buildDomainSeparator(); } - /// @inheritdoc IEIP712 + /// @inheritdoc IERC712 function nonce(address user) public view virtual returns (uint256) { return _nonces[user]; } @@ -52,7 +52,7 @@ contract EIP712 is IEIP712 { /// @dev Verifies a signature components against the provided data hash, nonce, deadline and signer. /// @param signature The signature to verify. - /// @param dataHash The EIP712 message hash the signature should correspond to. + /// @param dataHash The ERC712 message hash the signature should correspond to. /// @param signedNonce The nonce used along with the provided signature. Must not be an end-user input and must be proven to be signed by the signer. /// @param deadline The signature's maximum valid timestamp. Must not be an end-user input and must be proven to be signed by the signer. /// @param signer The expected signature's signer. @@ -88,11 +88,11 @@ contract EIP712 is IEIP712 { /// @notice Builds a domain separator using the current chainId and contract address. function _buildDomainSeparator() private view returns (bytes32) { - return keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, _NAMEHASH, block.chainid, address(this))); + return keccak256(abi.encode(ERC712_DOMAIN_TYPEHASH, _NAMEHASH, block.chainid, address(this))); } /// @notice Creates an EIP-712 typed data hash function _hashTypedData(bytes32 dataHash) private view returns (bytes32) { - return keccak256(abi.encodePacked(EIP712_MSG_PREFIX, DOMAIN_SEPARATOR(), dataHash)); + return keccak256(abi.encodePacked(ERC712_MSG_PREFIX, DOMAIN_SEPARATOR(), dataHash)); } } diff --git a/src/interfaces/IEIP712.sol b/src/interfaces/IERC712.sol similarity index 96% rename from src/interfaces/IEIP712.sol rename to src/interfaces/IERC712.sol index 801ba26..0343796 100644 --- a/src/interfaces/IEIP712.sol +++ b/src/interfaces/IERC712.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -interface IEIP712 { +interface IERC712 { /* STRUCTS */ struct Signature { @@ -38,6 +38,6 @@ interface IEIP712 { /// @dev Uses cached version if chainid and address are unchanged from construction. function DOMAIN_SEPARATOR() external view returns (bytes32); - /// @notice Returns the given signer's nonce to be used in the next EIP712 signature. + /// @notice Returns the given signer's nonce to be used in the next ERC712 signature. function nonce(address signer) external view returns (uint256); } diff --git a/test/TestEIP712.sol b/test/TestERC712.sol similarity index 58% rename from test/TestEIP712.sol rename to test/TestERC712.sol index bf41458..b2b6b21 100644 --- a/test/TestEIP712.sol +++ b/test/TestERC712.sol @@ -1,26 +1,26 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IEIP712} from "src/interfaces/IEIP712.sol"; +import {IERC712} from "src/interfaces/IERC712.sol"; -import {EIP712_DOMAIN_TYPEHASH, MAX_VALID_ECDSA_S} from "src/EIP712.sol"; -import {EIP712Mock} from "test/mocks/EIP712Mock.sol"; +import {ERC712_DOMAIN_TYPEHASH, MAX_VALID_ECDSA_S} from "src/ERC712.sol"; +import {ERC712Mock} from "test/mocks/ERC712Mock.sol"; import "forge-std/Test.sol"; -contract TestEIP712 is Test { +contract TestERC712 is Test { string constant NAME = "Test"; - EIP712Mock mock; + ERC712Mock mock; function setUp() public { - mock = new EIP712Mock(NAME); + mock = new ERC712Mock(NAME); } function testDomainSeparator() public { assertEq( - IEIP712(mock).DOMAIN_SEPARATOR(), - keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, keccak256(bytes(NAME)), block.chainid, address(this))) + IERC712(mock).DOMAIN_SEPARATOR(), + keccak256(abi.encode(ERC712_DOMAIN_TYPEHASH, keccak256(bytes(NAME)), block.chainid, address(this))) ); } @@ -30,7 +30,7 @@ contract TestEIP712 is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, dataHash); - mock.verify(IEIP712.Signature({r: r, s: s, v: v}), dataHash, 0, deadline, vm.addr(privateKey)); + mock.verify(IERC712.Signature({r: r, s: s, v: v}), dataHash, 0, deadline, vm.addr(privateKey)); } function testVerifySignatureExpired(bytes32 dataHash, bytes32 r, uint256 s, uint256 deadline, address signer) @@ -39,50 +39,50 @@ contract TestEIP712 is Test { s = bound(s, 0, MAX_VALID_ECDSA_S); deadline = bound(deadline, 0, block.timestamp - 1); - vm.expectRevert(IEIP712.SignatureExpired.selector); - mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, 0, deadline, signer); + vm.expectRevert(IERC712.SignatureExpired.selector); + mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, 0, deadline, signer); - vm.expectRevert(IEIP712.SignatureExpired.selector); - mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, 0, deadline, signer); + vm.expectRevert(IERC712.SignatureExpired.selector); + mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, 0, deadline, signer); } function testVerifyInvalidValueS(bytes32 dataHash, bytes32 r, uint256 s, address signer) public { s = bound(s, MAX_VALID_ECDSA_S + 1, type(uint256).max); - vm.expectRevert(IEIP712.InvalidValueS.selector); - mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, 0, block.timestamp, signer); + vm.expectRevert(IERC712.InvalidValueS.selector); + mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, 0, block.timestamp, signer); - vm.expectRevert(IEIP712.InvalidValueS.selector); - mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, 0, block.timestamp, signer); + vm.expectRevert(IERC712.InvalidValueS.selector); + mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, 0, block.timestamp, signer); } function testVerifyInvalidValueV(bytes32 dataHash, bytes32 r, uint256 s, uint8 v, address signer) public { s = bound(s, 0, MAX_VALID_ECDSA_S); vm.assume(v != 27 && v != 28); - vm.expectRevert(IEIP712.InvalidValueV.selector); - mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: v}), dataHash, 0, block.timestamp, signer); + vm.expectRevert(IERC712.InvalidValueV.selector); + mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: v}), dataHash, 0, block.timestamp, signer); } function testVerifyInvalidNonce(bytes32 dataHash, bytes32 r, uint256 s, uint256 nonce, address signer) public { s = bound(s, 0, MAX_VALID_ECDSA_S); nonce = bound(nonce, 1, type(uint256).max); - vm.expectRevert(abi.encodeWithSelector(IEIP712.InvalidNonce.selector, 0)); - mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, nonce, block.timestamp, signer); + vm.expectRevert(abi.encodeWithSelector(IERC712.InvalidNonce.selector, 0)); + mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, nonce, block.timestamp, signer); - vm.expectRevert(abi.encodeWithSelector(IEIP712.InvalidNonce.selector, 0)); - mock.verify(IEIP712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, nonce, block.timestamp, signer); + vm.expectRevert(abi.encodeWithSelector(IERC712.InvalidNonce.selector, 0)); + mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, nonce, block.timestamp, signer); } function testVerifyInvalidSignature(bytes32 dataHash, uint256 s, uint256 deadline, address signer) public { deadline = bound(deadline, block.timestamp, type(uint256).max); s = bound(s, 0, MAX_VALID_ECDSA_S); - vm.expectRevert(abi.encodeWithSelector(IEIP712.InvalidSignature.selector, address(0))); - mock.verify(IEIP712.Signature({r: 0, s: bytes32(s), v: 27}), dataHash, 0, block.timestamp, signer); + vm.expectRevert(abi.encodeWithSelector(IERC712.InvalidSignature.selector, address(0))); + mock.verify(IERC712.Signature({r: 0, s: bytes32(s), v: 27}), dataHash, 0, block.timestamp, signer); - vm.expectRevert(abi.encodeWithSelector(IEIP712.InvalidSignature.selector, address(0))); - mock.verify(IEIP712.Signature({r: 0, s: bytes32(s), v: 28}), dataHash, 0, block.timestamp, signer); + vm.expectRevert(abi.encodeWithSelector(IERC712.InvalidSignature.selector, address(0))); + mock.verify(IERC712.Signature({r: 0, s: bytes32(s), v: 28}), dataHash, 0, block.timestamp, signer); } } diff --git a/test/mocks/EIP712Mock.sol b/test/mocks/ERC712Mock.sol similarity index 72% rename from test/mocks/EIP712Mock.sol rename to test/mocks/ERC712Mock.sol index 695037a..20cd90c 100644 --- a/test/mocks/EIP712Mock.sol +++ b/test/mocks/ERC712Mock.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {EIP712} from "src/EIP712.sol"; +import {ERC712} from "src/ERC712.sol"; -contract EIP712Mock is EIP712 { - constructor(string memory name) EIP712(name) {} +contract ERC712Mock is ERC712 { + constructor(string memory name) ERC712(name) {} function verify( Signature calldata signature, From 558489ed0bebdd5f3d3f2c2607f76f45e1636631 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 14:51:04 +0200 Subject: [PATCH 06/13] feat(erc3156x): added flash lender/borrower erc --- src/ERC3156xFlashLender.sol | 52 +++++++++++++++++++++++ src/interfaces/IERC3156xFlashBorrower.sol | 19 +++++++++ src/interfaces/IERC3156xFlashLender.sol | 45 ++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/ERC3156xFlashLender.sol create mode 100644 src/interfaces/IERC3156xFlashBorrower.sol create mode 100644 src/interfaces/IERC3156xFlashLender.sol diff --git a/src/ERC3156xFlashLender.sol b/src/ERC3156xFlashLender.sol new file mode 100644 index 0000000..8aa4878 --- /dev/null +++ b/src/ERC3156xFlashLender.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; +import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; + +import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; + +/// @dev The expected success hash returned by the FlashBorrower. +bytes32 constant FLASH_BORROWER_SUCCESS_HASH = keccak256("ERC3156xFlashBorrower.onFlashLoan"); + +contract ERC3156xFlashLender is IERC3156xFlashLender { + using SafeTransferLib for ERC20; + + /* PUBLIC */ + + /// @inheritdoc IERC3156xFlashLender + function maxFlashLoan(address token) public view virtual returns (uint256) { + return ERC20(token).balanceOf(address(this)); + } + + /// @inheritdoc IERC3156xFlashLender + function flashFee(address, uint256) public pure virtual returns (uint256) { + return 0; + } + + /// @inheritdoc IERC3156xFlashLender + function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) + public + virtual + returns (bytes memory returnData) + { + uint256 max = maxFlashLoan(token); + if (amount > max) revert FlashLoanTooLarge(max); + + ERC20(token).safeTransfer(address(receiver), amount); + + uint256 fee = flashFee(token, amount); + + bytes32 successHash; + (successHash, returnData) = receiver.onFlashLoan(msg.sender, token, amount, fee, data); + if (successHash != FLASH_BORROWER_SUCCESS_HASH) revert InvalidFlashData(flashData); + + _accrueFee(token, amount, fee); + + ERC20(token).safeTransferFrom(address(receiver), address(this), amount + fee); + } + + /* INTERNAL */ + + function _accrueFee(address token, uint256 amount, uint256 fee) internal virtual {} +} diff --git a/src/interfaces/IERC3156xFlashBorrower.sol b/src/interfaces/IERC3156xFlashBorrower.sol new file mode 100644 index 0000000..5e2aa56 --- /dev/null +++ b/src/interfaces/IERC3156xFlashBorrower.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +/// @dev Interface of the ERC3156x FlashBorrower, inspired by https://eips.ethereum.org/EIPS/eip-3156. +/// The FlashLender's `flashLoan` function now returns the FlashBorrower's return data. +interface IERC3156xFlashBorrower { + /** + * @dev Receive a flash loan. + * @param initiator The initiator of the loan. + * @param token The loan currency. + * @param amount The amount of tokens lent. + * @param fee The additional amount of tokens to repay. + * @param data Arbitrary data structure, intended to contain user-defined parameters. + * @return The keccak256 hash of "IERC3156FlashBorrower.onFlashLoan" and any additional arbitrary data. + */ + function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) + external + returns (bytes32, bytes memory); +} diff --git a/src/interfaces/IERC3156xFlashLender.sol b/src/interfaces/IERC3156xFlashLender.sol new file mode 100644 index 0000000..1d30254 --- /dev/null +++ b/src/interfaces/IERC3156xFlashLender.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (interfaces/IERC3156FlashLender.sol) + +pragma solidity ^0.8.0; + +import {IERC3156xFlashBorrower} from "./IERC3156xFlashBorrower.sol"; + +/// @dev Interface of the ERC3156x FlashLender, inspired by https://eips.ethereum.org/EIPS/eip-3156. +/// The FlashLender's `flashLoan` function now returns the FlashBorrower's return data. +interface IERC3156xFlashLender { + /* EVENTS */ + + /// @dev Emitted when a flash loan is initiated. + event FlashLoan(address indexed initiator, address indexed receiver, address indexed token, uint256 amount); + + /* ERRORS */ + + /// @dev Thrown when the requested flash loan amount is larger than the maximum flash loan allowed. + error FlashLoanTooLarge(uint256 maxFlashLoan); + + /// @dev Thrown when the FlashBorrower's success hash is invalid. + error InvalidSuccessHash(bytes32 successHash); + + /* FUNCTIONS */ + + /// @dev The amount of currency available to be lended. + /// @param token The loan currency. + /// @return The amount of `token` that can be borrowed. + function maxFlashLoan(address token) external view returns (uint256); + + /// @dev The fee to be charged for a given loan. + /// @param token The loan currency. + /// @param amount The amount of tokens lent. + /// @return The amount of `token` to be charged for the loan, on top of the returned principal. + function flashFee(address token, uint256 amount) external view returns (uint256); + + /// @dev Initiate a flash loan. + /// @param receiver The receiver of the tokens in the loan, and the receiver of the callback. + /// @param token The loan currency. + /// @param amount The amount of tokens lent. + /// @param data Arbitrary data structure, intended to contain user-defined parameters. + function flashLoan(IERC3156xFlashBorrower receiver, address token, uint256 amount, bytes calldata data) + external + returns (bytes memory); +} From a90238c0ab2116b9f4a2a8bb9906554e3162ddc0 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 14:51:12 +0200 Subject: [PATCH 07/13] forge install: solmate --- .gitmodules | 3 +++ lib/solmate | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/solmate diff --git a/.gitmodules b/.gitmodules index 6dcad44..dc6bac2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,3 +11,6 @@ path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable branch = v4.8.3 +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/transmissions11/solmate diff --git a/lib/solmate b/lib/solmate new file mode 160000 index 0000000..bfc9c25 --- /dev/null +++ b/lib/solmate @@ -0,0 +1 @@ +Subproject commit bfc9c25865a274a7827fea5abf6e4fb64fc64e6c From c221ee35e84376b4a8b2a9cd004ad55f118a6916 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 14:52:12 +0200 Subject: [PATCH 08/13] fix(erc3156): make it compile --- remappings.txt | 1 + src/ERC3156xFlashLender.sol | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/remappings.txt b/remappings.txt index bdfc2ce..70adcab 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,3 +3,4 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +solmate/=lib/solmate/src/ diff --git a/src/ERC3156xFlashLender.sol b/src/ERC3156xFlashLender.sol index 8aa4878..0727b00 100644 --- a/src/ERC3156xFlashLender.sol +++ b/src/ERC3156xFlashLender.sol @@ -25,7 +25,7 @@ contract ERC3156xFlashLender is IERC3156xFlashLender { } /// @inheritdoc IERC3156xFlashLender - function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) + function flashLoan(IERC3156xFlashBorrower receiver, address token, uint256 amount, bytes calldata data) public virtual returns (bytes memory returnData) @@ -39,7 +39,7 @@ contract ERC3156xFlashLender is IERC3156xFlashLender { bytes32 successHash; (successHash, returnData) = receiver.onFlashLoan(msg.sender, token, amount, fee, data); - if (successHash != FLASH_BORROWER_SUCCESS_HASH) revert InvalidFlashData(flashData); + if (successHash != FLASH_BORROWER_SUCCESS_HASH) revert InvalidSuccessHash(successHash); _accrueFee(token, amount, fee); From 60035db1e7e7f5194e8959c609a0b1440a55be49 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 15:34:28 +0200 Subject: [PATCH 09/13] test(erc712): fix erc712 tests --- src/ERC712.sol | 6 +++--- test/TestERC712.sol | 41 ++++++++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/ERC712.sol b/src/ERC712.sol index c37e082..d976724 100644 --- a/src/ERC712.sol +++ b/src/ERC712.sol @@ -68,13 +68,13 @@ contract ERC712 is IERC712 { // v ∈ {27, 28} (source: https://ethereum.github.io/yellowpaper/paper.pdf #308) if (signature.v != 27 && signature.v != 28) revert InvalidValueV(); - uint256 usedNonce = _useNonce(signer); - if (signedNonce != usedNonce) revert InvalidNonce(usedNonce); - bytes32 digest = _hashTypedData(dataHash); address recovered = ecrecover(digest, signature.v, signature.r, signature.s); if (recovered == address(0) || signer != recovered) revert InvalidSignature(recovered); + + uint256 usedNonce = _useNonce(signer); + if (signedNonce != usedNonce) revert InvalidNonce(usedNonce); } /// @dev Increments and returns the nonce that should have been used in the corresponding signature. diff --git a/test/TestERC712.sol b/test/TestERC712.sol index b2b6b21..6134d67 100644 --- a/test/TestERC712.sol +++ b/test/TestERC712.sol @@ -3,12 +3,16 @@ pragma solidity ^0.8.0; import {IERC712} from "src/interfaces/IERC712.sol"; -import {ERC712_DOMAIN_TYPEHASH, MAX_VALID_ECDSA_S} from "src/ERC712.sol"; +import {ERC712_MSG_PREFIX, ERC712_DOMAIN_TYPEHASH, MAX_VALID_ECDSA_S} from "src/ERC712.sol"; import {ERC712Mock} from "test/mocks/ERC712Mock.sol"; import "forge-std/Test.sol"; +uint256 constant SECP256K1_CURVE_ORDER = 115792089237316195423570985008687907852837564279074904382605163141518161494337; + contract TestERC712 is Test { + event NonceUsed(address indexed caller, address indexed signer, uint256 usedNonce); + string constant NAME = "Test"; ERC712Mock mock; @@ -17,20 +21,29 @@ contract TestERC712 is Test { mock = new ERC712Mock(NAME); } - function testDomainSeparator() public { + function testDomainSeparator(uint64 chainId) public { + vm.chainId(chainId); + assertEq( IERC712(mock).DOMAIN_SEPARATOR(), - keccak256(abi.encode(ERC712_DOMAIN_TYPEHASH, keccak256(bytes(NAME)), block.chainid, address(this))) + keccak256(abi.encode(ERC712_DOMAIN_TYPEHASH, keccak256(bytes(NAME)), chainId, address(mock))) ); } function testVerify(bytes32 dataHash, uint256 deadline, uint256 privateKey) public { - privateKey = bound(privateKey, 1, type(uint256).max); deadline = bound(deadline, block.timestamp, type(uint256).max); + privateKey = bound(privateKey, 1, SECP256K1_CURVE_ORDER - 1); + + address signer = vm.addr(privateKey); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(privateKey, keccak256(abi.encodePacked(ERC712_MSG_PREFIX, mock.DOMAIN_SEPARATOR(), dataHash))); + + vm.expectEmit(true, true, true, true, address(mock)); + emit NonceUsed(address(this), signer, 0); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, dataHash); + mock.verify(IERC712.Signature({r: r, s: s, v: v}), dataHash, 0, deadline, signer); - mock.verify(IERC712.Signature({r: r, s: s, v: v}), dataHash, 0, deadline, vm.addr(privateKey)); + assertEq(mock.nonce(signer), 1); } function testVerifySignatureExpired(bytes32 dataHash, bytes32 r, uint256 s, uint256 deadline, address signer) @@ -64,15 +77,14 @@ contract TestERC712 is Test { mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: v}), dataHash, 0, block.timestamp, signer); } - function testVerifyInvalidNonce(bytes32 dataHash, bytes32 r, uint256 s, uint256 nonce, address signer) public { - s = bound(s, 0, MAX_VALID_ECDSA_S); + function testVerifyInvalidNonce(bytes32 dataHash, uint256 nonce, uint256 privateKey) public { nonce = bound(nonce, 1, type(uint256).max); + privateKey = bound(privateKey, 1, SECP256K1_CURVE_ORDER - 1); - vm.expectRevert(abi.encodeWithSelector(IERC712.InvalidNonce.selector, 0)); - mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: 27}), dataHash, nonce, block.timestamp, signer); + (address signer, IERC712.Signature memory signature) = _sign(privateKey, dataHash); vm.expectRevert(abi.encodeWithSelector(IERC712.InvalidNonce.selector, 0)); - mock.verify(IERC712.Signature({r: r, s: bytes32(s), v: 28}), dataHash, nonce, block.timestamp, signer); + mock.verify(signature, dataHash, nonce, block.timestamp, signer); } function testVerifyInvalidSignature(bytes32 dataHash, uint256 s, uint256 deadline, address signer) public { @@ -85,4 +97,11 @@ contract TestERC712 is Test { vm.expectRevert(abi.encodeWithSelector(IERC712.InvalidSignature.selector, address(0))); mock.verify(IERC712.Signature({r: 0, s: bytes32(s), v: 28}), dataHash, 0, block.timestamp, signer); } + + function _sign(uint256 privateKey, bytes32 dataHash) internal returns (address, IERC712.Signature memory) { + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(privateKey, keccak256(abi.encodePacked(ERC712_MSG_PREFIX, mock.DOMAIN_SEPARATOR(), dataHash))); + + return (vm.addr(privateKey), IERC712.Signature({r: r, s: s, v: v})); + } } From 5a7ae7d759d3696309e6d118110ac7a2746dd2cf Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 16:55:27 +0200 Subject: [PATCH 10/13] test(erc3156x): test lender/borrower --- src/ERC3156xFlashBorrower.sol | 53 +++++++++++ src/interfaces/IERC3156xFlashBorrower.sol | 26 ++++-- test/TestERC3156xFlashBorrower.sol | 47 ++++++++++ test/TestERC3156xFlashLender.sol | 108 ++++++++++++++++++++++ test/TestERC712.sol | 2 +- test/mocks/ERC20Mock.sol | 8 ++ test/mocks/ERC3156xFlashBorrowerMock.sol | 14 +++ test/mocks/ERC3156xFlashLenderMock.sol | 6 ++ 8 files changed, 254 insertions(+), 10 deletions(-) create mode 100644 src/ERC3156xFlashBorrower.sol create mode 100644 test/TestERC3156xFlashBorrower.sol create mode 100644 test/TestERC3156xFlashLender.sol create mode 100644 test/mocks/ERC20Mock.sol create mode 100644 test/mocks/ERC3156xFlashBorrowerMock.sol create mode 100644 test/mocks/ERC3156xFlashLenderMock.sol diff --git a/src/ERC3156xFlashBorrower.sol b/src/ERC3156xFlashBorrower.sol new file mode 100644 index 0000000..2ea2761 --- /dev/null +++ b/src/ERC3156xFlashBorrower.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; +import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; + +import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; + +import {FLASH_BORROWER_SUCCESS_HASH} from "src/ERC3156xFlashLender.sol"; + +contract ERC3156xFlashBorrower is IERC3156xFlashBorrower { + using SafeTransferLib for ERC20; + + IERC3156xFlashLender private immutable _LENDER; + + constructor(IERC3156xFlashLender lender) { + _LENDER = lender; + } + + /* PUBLIC */ + + /// @inheritdoc IERC3156xFlashBorrower + function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) + public + virtual + returns (bytes32 successHash, bytes memory returnData) + { + _checkFlashLoan(initiator); + + (successHash, returnData) = _onFlashLoan(initiator, token, amount, fee, data); + + ERC20(token).safeApprove(address(_LENDER), amount + fee); + } + + /* INTERNAL */ + + function _checkFlashLoan(address initiator) internal view virtual { + if (msg.sender != address(_LENDER)) revert UnauthorizedLender(); + if (initiator != address(this)) revert UnauthorizedInitiator(); + } + + function _flashLoan(address token, uint256 amount, bytes calldata data) internal virtual returns (bytes memory) { + return _LENDER.flashLoan(this, token, amount, data); + } + + function _onFlashLoan(address, address, uint256, uint256, bytes calldata) + public + virtual + returns (bytes32, bytes memory) + { + return (FLASH_BORROWER_SUCCESS_HASH, bytes("")); + } +} diff --git a/src/interfaces/IERC3156xFlashBorrower.sol b/src/interfaces/IERC3156xFlashBorrower.sol index 5e2aa56..b0fe8e9 100644 --- a/src/interfaces/IERC3156xFlashBorrower.sol +++ b/src/interfaces/IERC3156xFlashBorrower.sol @@ -4,15 +4,23 @@ pragma solidity ^0.8.0; /// @dev Interface of the ERC3156x FlashBorrower, inspired by https://eips.ethereum.org/EIPS/eip-3156. /// The FlashLender's `flashLoan` function now returns the FlashBorrower's return data. interface IERC3156xFlashBorrower { - /** - * @dev Receive a flash loan. - * @param initiator The initiator of the loan. - * @param token The loan currency. - * @param amount The amount of tokens lent. - * @param fee The additional amount of tokens to repay. - * @param data Arbitrary data structure, intended to contain user-defined parameters. - * @return The keccak256 hash of "IERC3156FlashBorrower.onFlashLoan" and any additional arbitrary data. - */ + /* ERRORS */ + + /// @dev Thrown when the caller of the FlashBorrower's callback is not authorized. + error UnauthorizedLender(); + + /// @dev Thrown when the intiiator of the flash loan is not authorized. + error UnauthorizedInitiator(); + + /* FUNCTIONS */ + + /// @dev Receive a flash loan. + /// @param initiator The initiator of the loan. + /// @param token The loan currency. + /// @param amount The amount of tokens lent. + /// @param fee The additional amount of tokens to repay. + /// @param data Arbitrary data structure, intended to contain user-defined parameters. + /// @return The keccak256 hash of "IERC3156FlashBorrower.onFlashLoan" and any additional arbitrary data. function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32, bytes memory); diff --git a/test/TestERC3156xFlashBorrower.sol b/test/TestERC3156xFlashBorrower.sol new file mode 100644 index 0000000..94730ca --- /dev/null +++ b/test/TestERC3156xFlashBorrower.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; +import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; + +import {ERC20Mock} from "test/mocks/ERC20Mock.sol"; +import {ERC3156xFlashLenderMock} from "test/mocks/ERC3156xFlashLenderMock.sol"; +import {ERC3156xFlashBorrowerMock} from "test/mocks/ERC3156xFlashBorrowerMock.sol"; + +import "forge-std/Test.sol"; + +contract TestERC3156xFlashBorrower is Test { + ERC20Mock token1; + ERC20Mock token2; + IERC3156xFlashLender lender; + ERC3156xFlashBorrowerMock borrower; + + function setUp() public { + token1 = new ERC20Mock("Test Token 1", "TT1"); + token2 = new ERC20Mock("Test Token 2", "TT2"); + lender = new ERC3156xFlashLenderMock(); + borrower = new ERC3156xFlashBorrowerMock(lender); + + deal(address(token1), address(lender), type(uint256).max); + } + + function testFlashLoan(uint256 amount) public { + borrower.flashLoan(address(token1), amount, bytes("")); + } + + function testFlashLoanUnauthorizedLender(address _lender, address token, uint256 amount, uint256 fee) public { + vm.assume(_lender != address(lender)); + + vm.prank(_lender); + vm.expectRevert(IERC3156xFlashBorrower.UnauthorizedLender.selector); + borrower.onFlashLoan(address(borrower), token, amount, fee, bytes("")); + } + + function testFlashLoanUnauthorizedInitiator(address initiator, uint256 amount) public { + vm.assume(initiator != address(borrower)); + + vm.prank(initiator); + vm.expectRevert(IERC3156xFlashBorrower.UnauthorizedInitiator.selector); + lender.flashLoan(borrower, address(token1), amount, bytes("")); + } +} diff --git a/test/TestERC3156xFlashLender.sol b/test/TestERC3156xFlashLender.sol new file mode 100644 index 0000000..818cebc --- /dev/null +++ b/test/TestERC3156xFlashLender.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; +import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; + +import {FLASH_BORROWER_SUCCESS_HASH} from "src/ERC3156xFlashLender.sol"; +import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; + +import {ERC20Mock} from "test/mocks/ERC20Mock.sol"; +import {ERC3156xFlashLenderMock} from "test/mocks/ERC3156xFlashLenderMock.sol"; + +import "forge-std/Test.sol"; + +contract TestERC3156xFlashLenderBase is Test, IERC3156xFlashBorrower { + ERC20Mock token1; + ERC20Mock token2; + IERC3156xFlashLender lender; + + function setUp() public { + token1 = new ERC20Mock("Test Token 1", "TT1"); + token2 = new ERC20Mock("Test Token 2", "TT2"); + lender = new ERC3156xFlashLenderMock(); + + deal(address(token1), address(lender), type(uint256).max); + } + + function onFlashLoan(address, address, uint256, uint256, bytes calldata) + public + virtual + returns (bytes32, bytes memory) + { + return (FLASH_BORROWER_SUCCESS_HASH, bytes("")); + } +} + +contract TestERC3156xFlashLender is TestERC3156xFlashLenderBase { + using SafeTransferLib for ERC20; + + function testFlashLoanTooLarge(address initiator, uint256 amount) public { + amount = bound(amount, 1, type(uint256).max); + + vm.prank(initiator); + vm.expectRevert(abi.encodeWithSelector(IERC3156xFlashLender.FlashLoanTooLarge.selector, 0)); + lender.flashLoan(this, address(token2), amount, bytes("")); + } +} + +contract TestERC3156xFlashLenderSuccess is TestERC3156xFlashLenderBase { + using SafeTransferLib for ERC20; + + address expectedInitiator; + uint256 expectedAmount; + uint256 expectedFee; + bytes expectedData; + + function testFlashLoan(address initiator, uint256 amount) public { + expectedInitiator = initiator; + expectedAmount = amount; + expectedFee = lender.flashFee(address(token1), amount); + expectedData = bytes("Hello"); + + vm.prank(initiator); + lender.flashLoan(this, address(token1), amount, expectedData); + } + + function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) + public + virtual + override + returns (bytes32, bytes memory) + { + assertEq(initiator, expectedInitiator, "initiator"); + assertEq(token, address(token1), "token"); + assertEq(amount, expectedAmount, "amount"); + assertEq(fee, expectedFee, "fee"); + assertEq(data, expectedData, "data"); + + assertEq(ERC20(token).balanceOf(address(this)), amount, "balanceOf"); + + ERC20(token).safeApprove(msg.sender, amount + fee); + + return (FLASH_BORROWER_SUCCESS_HASH, bytes("")); + } +} + +contract TestERC3156xFlashLenderFailure is TestERC3156xFlashLenderBase { + using SafeTransferLib for ERC20; + + function testFlashLoanInvalidSuccessHash(address initiator, uint256 amount, bytes32 successHash) public { + vm.assume(successHash != FLASH_BORROWER_SUCCESS_HASH); + + vm.prank(initiator); + vm.expectRevert(abi.encodeWithSelector(IERC3156xFlashLender.InvalidSuccessHash.selector, successHash)); + lender.flashLoan(this, address(token1), amount, abi.encode(successHash)); + } + + function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata data) + public + virtual + override + returns (bytes32, bytes memory) + { + ERC20(token).safeApprove(msg.sender, amount + fee); + + return (abi.decode(data, (bytes32)), bytes("")); + } +} diff --git a/test/TestERC712.sol b/test/TestERC712.sol index 6134d67..f904ff9 100644 --- a/test/TestERC712.sol +++ b/test/TestERC712.sol @@ -25,7 +25,7 @@ contract TestERC712 is Test { vm.chainId(chainId); assertEq( - IERC712(mock).DOMAIN_SEPARATOR(), + mock.DOMAIN_SEPARATOR(), keccak256(abi.encode(ERC712_DOMAIN_TYPEHASH, keccak256(bytes(NAME)), chainId, address(mock))) ); } diff --git a/test/mocks/ERC20Mock.sol b/test/mocks/ERC20Mock.sol new file mode 100644 index 0000000..6593bd9 --- /dev/null +++ b/test/mocks/ERC20Mock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract ERC20Mock is ERC20 { + constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol, 18) {} +} diff --git a/test/mocks/ERC3156xFlashBorrowerMock.sol b/test/mocks/ERC3156xFlashBorrowerMock.sol new file mode 100644 index 0000000..23a9bde --- /dev/null +++ b/test/mocks/ERC3156xFlashBorrowerMock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; + +import {ERC3156xFlashBorrower, FLASH_BORROWER_SUCCESS_HASH} from "src/ERC3156xFlashBorrower.sol"; + +contract ERC3156xFlashBorrowerMock is ERC3156xFlashBorrower { + constructor(IERC3156xFlashLender lender) ERC3156xFlashBorrower(lender) {} + + function flashLoan(address token, uint256 amount, bytes calldata data) public returns (bytes memory) { + return _flashLoan(token, amount, data); + } +} diff --git a/test/mocks/ERC3156xFlashLenderMock.sol b/test/mocks/ERC3156xFlashLenderMock.sol new file mode 100644 index 0000000..2253c6c --- /dev/null +++ b/test/mocks/ERC3156xFlashLenderMock.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {ERC3156xFlashLender} from "src/ERC3156xFlashLender.sol"; + +contract ERC3156xFlashLenderMock is ERC3156xFlashLender {} From a826a08429fd118e75523c0fb8192252ceb92474 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 17:13:45 +0200 Subject: [PATCH 11/13] feat(erc2330): add erc2330 alternative --- src/ERC2330.sol | 30 ++++++++++++++++++++++++++++++ src/interfaces/IERC2330.sol | 12 ++++++++++++ test/TestERC2330.sol | 36 ++++++++++++++++++++++++++++++++++++ test/mocks/ERC2330Mock.sol | 16 ++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 src/ERC2330.sol create mode 100644 src/interfaces/IERC2330.sol create mode 100644 test/TestERC2330.sol create mode 100644 test/mocks/ERC2330Mock.sol diff --git a/src/ERC2330.sol b/src/ERC2330.sol new file mode 100644 index 0000000..991ec29 --- /dev/null +++ b/src/ERC2330.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IERC2330} from "src/interfaces/IERC2330.sol"; + +/// @dev Gas-optimized extsload getters to allow anyone to read storage from this contract. +/// Enables the benefit of https://eips.ethereum.org/EIPS/eip-2330 without requiring changes to the execution layer. +contract ERC2330 is IERC2330 { + /* EXTERNAL */ + + /// @inheritdoc IERC2330 + function extsload(bytes32 slot) external view returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + value := sload(slot) + } + } + + /// @inheritdoc IERC2330 + function extsload(bytes32 startSlot, uint256 nSlots) external view returns (bytes memory value) { + value = new bytes(32 * nSlots); + + /// @solidity memory-safe-assembly + assembly { + for { let i := 0 } lt(i, nSlots) { i := add(i, 1) } { + mstore(add(value, mul(add(i, 1), 32)), sload(add(startSlot, i))) + } + } + } +} diff --git a/src/interfaces/IERC2330.sol b/src/interfaces/IERC2330.sol new file mode 100644 index 0000000..4facb6e --- /dev/null +++ b/src/interfaces/IERC2330.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +interface IERC2330 { + /* FUNCTIONS */ + + /// @dev Returns the 32-bytes value stored in this contract, at the given storage slot. + function extsload(bytes32 slot) external view returns (bytes32 value); + + /// @dev Returns the `nSlots` 32-bytes values stored in this contract, starting from the given start slot. + function extsload(bytes32 startSlot, uint256 nSlots) external view returns (bytes memory value); +} diff --git a/test/TestERC2330.sol b/test/TestERC2330.sol new file mode 100644 index 0000000..65009c1 --- /dev/null +++ b/test/TestERC2330.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IERC2330} from "src/interfaces/IERC2330.sol"; + +import {ERC2330Mock} from "test/mocks/ERC2330Mock.sol"; + +import "forge-std/Test.sol"; + +contract TestERC2330 is Test { + ERC2330Mock mock; + + function setUp() public { + mock = new ERC2330Mock(); + } + + function testExtsload() public { + assertEq( + mock.extsload(0x0000000000000000000000000000000000000000000000000000000000000000), + 0x0000000000000000000000000000000100000000000000000000000000000001 + ); + assertEq( + mock.extsload(0x0000000000000000000000000000000000000000000000000000000000000001), + 0x0000000000000000000000000000000000000000000000000000000000000002 + ); + } + + function testExtsloadMultiple() public { + (bytes32 var1, bytes32 var2) = abi.decode( + mock.extsload(0x0000000000000000000000000000000000000000000000000000000000000000, 2), (bytes32, bytes32) + ); + + assertEq(var1, 0x0000000000000000000000000000000100000000000000000000000000000001); + assertEq(var2, 0x0000000000000000000000000000000000000000000000000000000000000002); + } +} diff --git a/test/mocks/ERC2330Mock.sol b/test/mocks/ERC2330Mock.sol new file mode 100644 index 0000000..984e31c --- /dev/null +++ b/test/mocks/ERC2330Mock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {ERC2330} from "src/ERC2330.sol"; + +contract ERC2330Mock is ERC2330 { + uint128 private _var11; + uint128 private _var12; + uint256 private _var2; + + constructor() { + _var11 = 1; + _var12 = 1; + _var2 = 2; + } +} From 97080d1a81920c86070864c3fdd795edcb19c4f4 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 20 Jun 2023 17:19:10 +0200 Subject: [PATCH 12/13] refacgtor(remappings): update solmate remapping --- remappings.txt | 3 ++- src/ERC3156xFlashBorrower.sol | 2 +- src/ERC3156xFlashLender.sol | 2 +- test/TestERC3156xFlashLender.sol | 2 +- test/mocks/ERC20Mock.sol | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/remappings.txt b/remappings.txt index 70adcab..c343a0d 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,4 +3,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ -solmate/=lib/solmate/src/ + +@solmate/=lib/solmate/src/ diff --git a/src/ERC3156xFlashBorrower.sol b/src/ERC3156xFlashBorrower.sol index 2ea2761..8829dbc 100644 --- a/src/ERC3156xFlashBorrower.sol +++ b/src/ERC3156xFlashBorrower.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; -import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; +import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; import {FLASH_BORROWER_SUCCESS_HASH} from "src/ERC3156xFlashLender.sol"; diff --git a/src/ERC3156xFlashLender.sol b/src/ERC3156xFlashLender.sol index 0727b00..f0e944d 100644 --- a/src/ERC3156xFlashLender.sol +++ b/src/ERC3156xFlashLender.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; -import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; +import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; /// @dev The expected success hash returned by the FlashBorrower. bytes32 constant FLASH_BORROWER_SUCCESS_HASH = keccak256("ERC3156xFlashBorrower.onFlashLoan"); diff --git a/test/TestERC3156xFlashLender.sol b/test/TestERC3156xFlashLender.sol index 818cebc..2c46749 100644 --- a/test/TestERC3156xFlashLender.sol +++ b/test/TestERC3156xFlashLender.sol @@ -5,7 +5,7 @@ import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; import {FLASH_BORROWER_SUCCESS_HASH} from "src/ERC3156xFlashLender.sol"; -import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; +import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; import {ERC20Mock} from "test/mocks/ERC20Mock.sol"; import {ERC3156xFlashLenderMock} from "test/mocks/ERC3156xFlashLenderMock.sol"; diff --git a/test/mocks/ERC20Mock.sol b/test/mocks/ERC20Mock.sol index 6593bd9..b68e49b 100644 --- a/test/mocks/ERC20Mock.sol +++ b/test/mocks/ERC20Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ERC20} from "@solmate/tokens/ERC20.sol"; contract ERC20Mock is ERC20 { constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol, 18) {} From 74f194356d1eb131fcd1b35ece16a01e7c0d0386 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 21 Jun 2023 16:43:35 +0200 Subject: [PATCH 13/13] fix(imports): use . instead of src --- src/ERC2330.sol | 2 +- src/ERC3156xFlashBorrower.sol | 6 +++--- src/ERC3156xFlashLender.sol | 4 ++-- src/ERC712.sol | 2 +- src/access/Ownable.sol | 2 +- src/access/Ownable2Step.sol | 4 ++-- src/interfaces/IERC3156xFlashBorrower.sol | 2 +- src/interfaces/access/IOwnable2Step.sol | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ERC2330.sol b/src/ERC2330.sol index 991ec29..3b126cc 100644 --- a/src/ERC2330.sol +++ b/src/ERC2330.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IERC2330} from "src/interfaces/IERC2330.sol"; +import {IERC2330} from "./interfaces/IERC2330.sol"; /// @dev Gas-optimized extsload getters to allow anyone to read storage from this contract. /// Enables the benefit of https://eips.ethereum.org/EIPS/eip-2330 without requiring changes to the execution layer. diff --git a/src/ERC3156xFlashBorrower.sol b/src/ERC3156xFlashBorrower.sol index 8829dbc..37cb5ea 100644 --- a/src/ERC3156xFlashBorrower.sol +++ b/src/ERC3156xFlashBorrower.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; -import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; +import {IERC3156xFlashLender} from "./interfaces/IERC3156xFlashLender.sol"; +import {IERC3156xFlashBorrower} from "./interfaces/IERC3156xFlashBorrower.sol"; import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; -import {FLASH_BORROWER_SUCCESS_HASH} from "src/ERC3156xFlashLender.sol"; +import {FLASH_BORROWER_SUCCESS_HASH} from "./ERC3156xFlashLender.sol"; contract ERC3156xFlashBorrower is IERC3156xFlashBorrower { using SafeTransferLib for ERC20; diff --git a/src/ERC3156xFlashLender.sol b/src/ERC3156xFlashLender.sol index f0e944d..77f6cb1 100644 --- a/src/ERC3156xFlashLender.sol +++ b/src/ERC3156xFlashLender.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IERC3156xFlashLender} from "src/interfaces/IERC3156xFlashLender.sol"; -import {IERC3156xFlashBorrower} from "src/interfaces/IERC3156xFlashBorrower.sol"; +import {IERC3156xFlashLender} from "./interfaces/IERC3156xFlashLender.sol"; +import {IERC3156xFlashBorrower} from "./interfaces/IERC3156xFlashBorrower.sol"; import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; diff --git a/src/ERC712.sol b/src/ERC712.sol index d976724..0106e70 100644 --- a/src/ERC712.sol +++ b/src/ERC712.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IERC712} from "src/interfaces/IERC712.sol"; +import {IERC712} from "./interfaces/IERC712.sol"; /// @dev The prefix used for EIP-712 signature. string constant ERC712_MSG_PREFIX = "\x19\x01"; diff --git a/src/access/Ownable.sol b/src/access/Ownable.sol index ca64540..d7052ed 100644 --- a/src/access/Ownable.sol +++ b/src/access/Ownable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IOwnable} from "src/interfaces/access/IOwnable.sol"; +import {IOwnable} from "../interfaces/access/IOwnable.sol"; /// @notice Gas-optimized Ownable helpers. /// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol diff --git a/src/access/Ownable2Step.sol b/src/access/Ownable2Step.sol index 99bebe8..b2c34d5 100644 --- a/src/access/Ownable2Step.sol +++ b/src/access/Ownable2Step.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IOwnable2Step} from "src/interfaces/access/IOwnable2Step.sol"; +import {IOwnable2Step} from "../interfaces/access/IOwnable2Step.sol"; -import {Ownable} from "src/access/Ownable.sol"; +import {Ownable} from "./Ownable.sol"; /// @notice Gas-optimized Ownable2Step helpers. /// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable2Step.sol diff --git a/src/interfaces/IERC3156xFlashBorrower.sol b/src/interfaces/IERC3156xFlashBorrower.sol index b0fe8e9..b270e84 100644 --- a/src/interfaces/IERC3156xFlashBorrower.sol +++ b/src/interfaces/IERC3156xFlashBorrower.sol @@ -20,7 +20,7 @@ interface IERC3156xFlashBorrower { /// @param amount The amount of tokens lent. /// @param fee The additional amount of tokens to repay. /// @param data Arbitrary data structure, intended to contain user-defined parameters. - /// @return The keccak256 hash of "IERC3156FlashBorrower.onFlashLoan" and any additional arbitrary data. + /// @return The keccak256 hash of "IERC3156xFlashBorrower.onFlashLoan" and any additional arbitrary data. function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32, bytes memory); diff --git a/src/interfaces/access/IOwnable2Step.sol b/src/interfaces/access/IOwnable2Step.sol index 3770155..22fd8a3 100644 --- a/src/interfaces/access/IOwnable2Step.sol +++ b/src/interfaces/access/IOwnable2Step.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {IOwnable} from "src/interfaces/access/IOwnable.sol"; +import {IOwnable} from "./IOwnable.sol"; interface IOwnable2Step is IOwnable { /* EVENTS */