Skip to content

Add ERC712, ERC2330, ERC3156x, Ownable & Ownable2Step #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/solmate
Submodule solmate added at bfc9c2
2 changes: 2 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +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/
30 changes: 30 additions & 0 deletions src/ERC2330.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

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.
contract ERC2330 is IERC2330 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be appropriate to set these contracts as abstract?

/* 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)))
}
}
}
}
53 changes: 53 additions & 0 deletions src/ERC3156xFlashBorrower.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

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 "./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(""));
}
}
52 changes: 52 additions & 0 deletions src/ERC3156xFlashLender.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import {IERC3156xFlashLender} from "./interfaces/IERC3156xFlashLender.sol";
import {IERC3156xFlashBorrower} from "./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(IERC3156xFlashBorrower 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 InvalidSuccessHash(successHash);

_accrueFee(token, amount, fee);

ERC20(token).safeTransferFrom(address(receiver), address(this), amount + fee);
}

/* INTERNAL */

function _accrueFee(address token, uint256 amount, uint256 fee) internal virtual {}
}
98 changes: 98 additions & 0 deletions src/ERC712.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import {IERC712} from "./interfaces/IERC712.sol";

/// @dev The prefix used for EIP-712 signature.
string constant ERC712_MSG_PREFIX = "\x19\x01";

/// @dev The domain typehash used for the EIP-712 signature.
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 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/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;

/// @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;

/// @dev The nonce used inside by signers to offer signature replay protection.
mapping(address => uint256) private _nonces;

constructor(string memory name) {
_NAMEHASH = keccak256(bytes(name));

_CACHED_CHAIN_ID = block.chainid;
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator();
}

/* PUBLIC */

/// @inheritdoc IERC712
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == _CACHED_CHAIN_ID ? _CACHED_DOMAIN_SEPARATOR : _buildDomainSeparator();
}

/// @inheritdoc IERC712
function nonce(address user) public view virtual returns (uint256) {
return _nonces[user];
}

/* INTERNAL */

/// @dev Verifies a signature components against the provided data hash, nonce, deadline and signer.
/// @param signature The signature to verify.
/// @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.
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();

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.
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() private view returns (bytes32) {
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(ERC712_MSG_PREFIX, DOMAIN_SEPARATOR(), dataHash));
}
}
52 changes: 52 additions & 0 deletions src/access/Ownable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

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
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 */

/// @inheritdoc IOwnable
function owner() public view virtual returns (address) {
return _owner;
}

/// @inheritdoc IOwnable
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);
}
}
48 changes: 48 additions & 0 deletions src/access/Ownable2Step.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import {IOwnable2Step} from "../interfaces/access/IOwnable2Step.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
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 */

/// @inheritdoc IOwnable2Step
function pendingOwner() public view virtual returns (address) {
return _pendingOwner;
}

/// @inheritdoc IOwnable2Step
function acceptOwnership() public virtual {
address sender = msg.sender;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to cache the sender because msg.sender is actually cheaper than using memory.


address pending = pendingOwner();
if (pending != sender) revert PendingOwnershipRequired(pending);

_transferOwnership(sender);
}

/// @inheritdoc IOwnable2Step
function transferOwnership(address newOwner) public virtual override(IOwnable2Step, Ownable) 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);
}
}
12 changes: 12 additions & 0 deletions src/interfaces/IERC2330.sol
Original file line number Diff line number Diff line change
@@ -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);
}
27 changes: 27 additions & 0 deletions src/interfaces/IERC3156xFlashBorrower.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 {
/* 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 "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);
}
Loading