diff --git a/CHANGELOG.md b/CHANGELOG.md index a814b447da0..1ee74687f15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * `IERC20Metadata`: add a new extended interface that includes the optional `name()`, `symbol()` and `decimals()` functions. ([#2561](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2561)) * `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552)) * `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565)) + * `ERC20FlashMint`: add an implementation of the ERC3156 extension for flash-minting ERC20 tokens. ([#2543](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2543)) + * `SignatureChecker`: add a signature verification library that supports both EOA and ERC1271 compliant contracts as signers. ([#2532](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2532)) * `Multicall`: add abstract contract with `multicall(bytes[] calldata data)` function to bundle multiple calls together ([#2608](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2608)) ## 4.0.0 (2021-03-23) diff --git a/contracts/interfaces/IERC1271.sol b/contracts/interfaces/IERC1271.sol new file mode 100644 index 00000000000..ca5de924cc5 --- /dev/null +++ b/contracts/interfaces/IERC1271.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC1271 standard signature validation method for + * contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271]. + */ +interface IERC1271 { + /** + * @dev Should return whether the signature provided is valid for the provided data + * @param hash Hash of the data to be signed + * @param signature Signature byte array associated with _data + */ + function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); +} diff --git a/contracts/interfaces/IERC3156.sol b/contracts/interfaces/IERC3156.sol new file mode 100644 index 00000000000..2f3d846080b --- /dev/null +++ b/contracts/interfaces/IERC3156.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC3156 FlashBorrower, as defined in + * https://eips.ethereum.org/EIPS/eip-3156[ERC-3156]. + */ +interface IERC3156FlashBorrower { + /** + * @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 "ERC3156FlashBorrower.onFlashLoan" + */ + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) external returns (bytes32); +} + +/** + * @dev Interface of the ERC3156 FlashLender, as defined in + * https://eips.ethereum.org/EIPS/eip-3156[ERC-3156]. + */ +interface IERC3156FlashLender { + /** + * @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( + IERC3156FlashBorrower receiver, + address token, + uint256 amount, + bytes calldata data + ) external returns (bool); + } diff --git a/contracts/mocks/ERC1271WalletMock.sol b/contracts/mocks/ERC1271WalletMock.sol new file mode 100644 index 00000000000..c92acdba63e --- /dev/null +++ b/contracts/mocks/ERC1271WalletMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../access/Ownable.sol"; +import "../interfaces/IERC1271.sol"; +import "../utils/cryptography/ECDSA.sol"; + +contract ERC1271WalletMock is Ownable, IERC1271 { + constructor(address originalOwner) { + transferOwnership(originalOwner); + } + + function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4 magicValue) { + return ECDSA.recover(hash, signature) == owner() ? this.isValidSignature.selector : bytes4(0); + } +} diff --git a/contracts/mocks/ERC3156FlashBorrowerMock.sol b/contracts/mocks/ERC3156FlashBorrowerMock.sol new file mode 100644 index 00000000000..63eb8ef2ee3 --- /dev/null +++ b/contracts/mocks/ERC3156FlashBorrowerMock.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + + +import "../token/ERC20/IERC20.sol"; +import "../interfaces/IERC3156.sol"; +import "../utils/Address.sol"; + +/** + * @dev WARNING: this IERC3156FlashBorrower mock implementation is for testing purposes ONLY. + * Writing a secure flash lock borrower is not an easy task, and should be done with the utmost care. + * This is not an example of how it should be done, and no pattern present in this mock should be considered secure. + * Following best practices, always have your contract properly audited before using them to manipulate important funds on + * live networks. + */ +contract ERC3156FlashBorrowerMock is IERC3156FlashBorrower { + bytes32 constant internal RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + bool immutable _enableApprove; + bool immutable _enableReturn; + + event BalanceOf(address token, address account, uint256 value); + event TotalSupply(address token, uint256 value); + + constructor(bool enableReturn, bool enableApprove) { + _enableApprove = enableApprove; + _enableReturn = enableReturn; + } + + function onFlashLoan( + address /*initiator*/, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) public override returns (bytes32) { + require(msg.sender == token); + + emit BalanceOf(token, address(this), IERC20(token).balanceOf(address(this))); + emit TotalSupply(token, IERC20(token).totalSupply()); + + if (data.length > 0) { + // WARNING: This code is for testing purposes only! Do not use. + Address.functionCall(token, data); + } + + if (_enableApprove) { + IERC20(token).approve(token, amount + fee); + } + + return _enableReturn ? RETURN_VALUE : bytes32(0); + } +} diff --git a/contracts/mocks/ERC3156Mock.sol b/contracts/mocks/ERC3156Mock.sol new file mode 100644 index 00000000000..ccc344141af --- /dev/null +++ b/contracts/mocks/ERC3156Mock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + + +import "../token/ERC20/extensions/draft-ERC20FlashMint.sol"; + +contract ERC20FlashMintMock is ERC20FlashMint { + constructor ( + string memory name, + string memory symbol, + address initialAccount, + uint256 initialBalance + ) ERC20(name, symbol) { + _mint(initialAccount, initialBalance); + } +} diff --git a/contracts/mocks/SignatureCheckerMock.sol b/contracts/mocks/SignatureCheckerMock.sol new file mode 100644 index 00000000000..5671540ec4a --- /dev/null +++ b/contracts/mocks/SignatureCheckerMock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/cryptography/SignatureChecker.sol"; + +contract SignatureCheckerMock { + using SignatureChecker for address; + + function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) public view returns (bool) { + return signer.isValidSignatureNow(hash, signature); + } +} diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 1c3fe308f56..1cd2ae9d4ba 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -76,7 +76,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * * Tokens usually opt for a value of 18, imitating the relationship between * Ether and Wei. This is the value {ERC20} uses, unless this function is - * overloaded; + * overridden; * * NOTE: This information is only used for _display_ purposes: it in * no way affects any of the arithmetic of the contract, including diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 223208c8b84..16f38b75754 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -15,21 +15,22 @@ There a few core contracts that implement the behavior specified in the EIP: Additionally there are multiple custom extensions, including: -* {ERC20Permit}: gasless approval of tokens. -* {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time. * {ERC20Burnable}: destruction of own tokens. * {ERC20Capped}: enforcement of a cap to the total supply when minting tokens. * {ERC20Pausable}: ability to pause token transfers. +* {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time. +* {ERC20Permit}: gasless approval of tokens (standardized as ERC2612). +* {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156). Finally, there are some utilities to interact with ERC20 contracts in various ways. * {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values. * {TokenTimelock}: hold tokens for a beneficiary until a specified time. -The following related EIPs are in draft status and can be found in the drafts directory. +The following related EIPs are in draft status. -- {IERC20Permit} - {ERC20Permit} +- {ERC20FlashMint} NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc20.adoc#Presets[ERC20 Presets] (such as {ERC20PresetMinterPauser}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts. @@ -43,22 +44,22 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel == Extensions -{{ERC20Snapshot}} - -{{ERC20Pausable}} - {{ERC20Burnable}} {{ERC20Capped}} +{{ERC20Pausable}} + +{{ERC20Snapshot}} + == Draft EIPs The following EIPs are still in Draft status. Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly. -{{IERC20Permit}} - {{ERC20Permit}} +{{ERC20FlashMint}} + == Presets These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code. diff --git a/contracts/token/ERC20/extensions/draft-ERC20FlashMint.sol b/contracts/token/ERC20/extensions/draft-ERC20FlashMint.sol new file mode 100644 index 00000000000..a4f2b04c73d --- /dev/null +++ b/contracts/token/ERC20/extensions/draft-ERC20FlashMint.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../../interfaces/IERC3156.sol"; +import "../ERC20.sol"; + +/** + * @dev Implementation of the ERC3156 Flash loans extension, as defined in + * https://eips.ethereum.org/EIPS/eip-3156[ERC-3156]. + * + * Adds the {flashLoan} method, which provides flash loan support at the token + * level. By default there is no fee, but this can be changed by overriding {flashFee}. + */ +abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { + bytes32 constant private RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + /** + * @dev Returns the maximum amount of tokens available for loan. + * @param token The address of the token that is requested. + * @return The amont of token that can be loaned. + */ + function maxFlashLoan(address token) public view override returns (uint256) { + return token == address(this) ? type(uint256).max - ERC20.totalSupply() : 0; + } + + /** + * @dev Returns the fee applied when doing flash loans. By default this + * implementation has 0 fees. This function can be overloaded to make + * the flash loan mechanism deflationary. + * @param token The token to be flash loaned. + * @param amount The amount of tokens to be loaned. + * @return The fees applied to the corresponding flash loan. + */ + function flashFee(address token, uint256 amount) public view virtual override returns (uint256) { + require(token == address(this), "ERC20FlashMint: wrong token"); + // silence warning about unused variable without the addition of bytecode. + amount; + return 0; + } + + /** + * @dev Performs a flash loan. New tokens are minted and sent to the + * `receiver`, who is required to implement the {IERC3156FlashBorrower} + * interface. By the end of the flash loan, the receiver is expected to own + * amount + fee tokens and have them approved back to the token contract itself so + * they can be burned. + * @param receiver The receiver of the flash loan. Should implement the + * {IERC3156FlashBorrower.onFlashLoan} interface. + * @param token The token to be flash loaned. Only `address(this)` is + * supported. + * @param amount The amount of tokens to be loaned. + * @param data An arbitrary datafield that is passed to the receiver. + * @return `true` is the flash loan was successfull. + */ + function flashLoan( + IERC3156FlashBorrower receiver, + address token, + uint256 amount, + bytes calldata data + ) + public virtual override returns (bool) + { + uint256 fee = flashFee(token, amount); + _mint(address(receiver), amount); + require(receiver.onFlashLoan(msg.sender, token, amount, fee, data) == RETURN_VALUE, "ERC20FlashMint: invalid return value"); + uint256 currentAllowance = allowance(address(receiver), address(this)); + require(currentAllowance >= amount + fee, "ERC20FlashMint: allowance does not allow refund"); + _approve(address(receiver), address(this), currentAllowance - amount - fee); + _burn(address(receiver), amount + fee); + return true; + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 1c39dc87e39..d475648090e 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -37,6 +37,8 @@ Finally, {Create2} contains all necessary utilities to safely use the https://bl {{ECDSA}} +{{SignatureChecker}} + {{MerkleProof}} {{EIP712}} diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol new file mode 100644 index 00000000000..881ae579a86 --- /dev/null +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./ECDSA.sol"; +import "../Address.sol"; +import "../../interfaces/IERC1271.sol"; + +/** + * @dev Signature verification helper: Provide a single mechanism to verify both private-key (EOA) ECDSA signature and + * ERC1271 contract sigantures. Using this instead of ECDSA.recover in your contract will make them compatible with + * smart contract wallets such as Argent and Gnosis. + * + * Note: unlike ECDSA signatures, contract signature's are revocable, and the outcome of this function can thus change + * through time. It could return true at block N and false at block N+1 (or the opposite). + */ +library SignatureChecker { + function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + if (Address.isContract(signer)) { + try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 magicValue) { + return magicValue == IERC1271(signer).isValidSignature.selector; + } catch { + return false; + } + } else { + return ECDSA.recover(hash, signature) == signer; + } + } +} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index c91043e55cd..f62cc2ef2d4 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,4 +1,5 @@ * xref:index.adoc[Overview] +* xref:wizard.adoc[Wizard] * xref:extending-contracts.adoc[Extending Contracts] * xref:upgradeable.adoc[Using with Upgrades] diff --git a/docs/modules/ROOT/pages/wizard.adoc b/docs/modules/ROOT/pages/wizard.adoc new file mode 100644 index 00000000000..6ea061891f2 --- /dev/null +++ b/docs/modules/ROOT/pages/wizard.adoc @@ -0,0 +1,15 @@ += Contracts Wizard +:page-notoc: + +Not sure where to start? Use the interactive generator below to bootstrap your +contract and learn about the components offered in OpenZeppelin Contracts. + +TIP: Place the resulting contract in your `contracts` directory in order to compile it with a tool like Hardhat or Truffle. Consider reading our guide on xref:learn::developing-smart-contracts.adoc[Developing Smart Contracts] for more guidance! + +++++ + + + +++++ + + diff --git a/test/token/ERC20/extensions/draft-ERC20FlashMint.test.js b/test/token/ERC20/extensions/draft-ERC20FlashMint.test.js new file mode 100644 index 00000000000..f7465a5ca5c --- /dev/null +++ b/test/token/ERC20/extensions/draft-ERC20FlashMint.test.js @@ -0,0 +1,90 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const ERC20FlashMintMock = artifacts.require('ERC20FlashMintMock'); +const ERC3156FlashBorrowerMock = artifacts.require('ERC3156FlashBorrowerMock'); + +contract('ERC20FlashMint', function (accounts) { + const [ initialHolder, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + + const initialSupply = new BN(100); + const loanAmount = new BN(10000000000000); + + beforeEach(async function () { + this.token = await ERC20FlashMintMock.new(name, symbol, initialHolder, initialSupply); + }); + + describe('maxFlashLoan', function () { + it('token match', async function () { + expect(await this.token.maxFlashLoan(this.token.address)).to.be.bignumber.equal(MAX_UINT256.sub(initialSupply)); + }); + + it('token mismatch', async function () { + expect(await this.token.maxFlashLoan(ZERO_ADDRESS)).to.be.bignumber.equal('0'); + }); + }); + + describe('flashFee', function () { + it('token match', async function () { + expect(await this.token.flashFee(this.token.address, loanAmount)).to.be.bignumber.equal('0'); + }); + + it('token mismatch', async function () { + await expectRevert(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC20FlashMint: wrong token'); + }); + }); + + describe('flashLoan', function () { + it('success', async function () { + const receiver = await ERC3156FlashBorrowerMock.new(true, true); + const { tx } = await this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'); + + expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, to: receiver.address, value: loanAmount }); + expectEvent.inTransaction(tx, this.token, 'Transfer', { from: receiver.address, to: ZERO_ADDRESS, value: loanAmount }); + expectEvent.inTransaction(tx, receiver, 'BalanceOf', { token: this.token.address, account: receiver.address, value: loanAmount }); + expectEvent.inTransaction(tx, receiver, 'TotalSupply', { token: this.token.address, value: initialSupply.add(loanAmount) }); + + expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); + expect(await this.token.balanceOf(receiver.address)).to.be.bignumber.equal('0'); + expect(await this.token.allowance(receiver.address, this.token.address)).to.be.bignumber.equal('0'); + }); + + it ('missing return value', async function () { + const receiver = await ERC3156FlashBorrowerMock.new(false, true); + await expectRevert( + this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'), + 'ERC20FlashMint: invalid return value', + ); + }); + + it ('missing approval', async function () { + const receiver = await ERC3156FlashBorrowerMock.new(true, false); + await expectRevert( + this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'), + 'ERC20FlashMint: allowance does not allow refund', + ); + }); + + it ('unavailable funds', async function () { + const receiver = await ERC3156FlashBorrowerMock.new(true, true); + const data = this.token.contract.methods.transfer(other, 10).encodeABI(); + await expectRevert( + this.token.flashLoan(receiver.address, this.token.address, loanAmount, data), + 'ERC20: burn amount exceeds balance', + ); + }); + + it ('more than maxFlashLoan', async function () { + const receiver = await ERC3156FlashBorrowerMock.new(true, true); + const data = this.token.contract.methods.transfer(other, 10).encodeABI(); + // _mint overflow reverts using a panic code. No reason string. + await expectRevert.unspecified(this.token.flashLoan(receiver.address, this.token.address, MAX_UINT256, data)); + }); + }); +}); diff --git a/test/utils/cryptography/SignatureChecker.test.js b/test/utils/cryptography/SignatureChecker.test.js new file mode 100644 index 00000000000..4364d150e11 --- /dev/null +++ b/test/utils/cryptography/SignatureChecker.test.js @@ -0,0 +1,71 @@ +const { toEthSignedMessageHash, fixSignature } = require('../../helpers/sign'); + +const { expect } = require('chai'); + +const SignatureCheckerMock = artifacts.require('SignatureCheckerMock'); +const ERC1271WalletMock = artifacts.require('ERC1271WalletMock'); + +const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin'); +const WRONG_MESSAGE = web3.utils.sha3('Nope'); + +contract('SignatureChecker (ERC1271)', function (accounts) { + const [signer, other] = accounts; + + before('deploying', async function () { + this.signaturechecker = await SignatureCheckerMock.new(); + this.wallet = await ERC1271WalletMock.new(signer); + this.signature = fixSignature(await web3.eth.sign(TEST_MESSAGE, signer)); + }); + + context('EOA account', function () { + it('with matching signer and signature', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + signer, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + )).to.equal(true); + }); + + it('with invalid signer', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + other, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + )).to.equal(false); + }); + + it('with invalid signature', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + signer, + toEthSignedMessageHash(WRONG_MESSAGE), + this.signature, + )).to.equal(false); + }); + }); + + context('ERC1271 wallet', function () { + it('with matching signer and signature', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + this.wallet.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + )).to.equal(true); + }); + + it('with invalid signer', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + this.signaturechecker.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + )).to.equal(false); + }); + + it('with invalid signature', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + this.wallet.address, + toEthSignedMessageHash(WRONG_MESSAGE), + this.signature, + )).to.equal(false); + }); + }); +});