فهرست منبع

Add ERC3156 extension of ERC20 (flash minting and lending) (#2543)

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
Hadrien Croubois 4 سال پیش
والد
کامیت
5bd798c6e1

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@
  * `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))
 
 ## 4.0.0 (2021-03-23)
 

+ 66 - 0
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);
+ }

+ 54 - 0
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);
+    }
+}

+ 17 - 0
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);
+    }
+}

+ 11 - 10
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 <<ERC20-_mint-address-uint256-,`_mint`>>) 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.

+ 73 - 0
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;
+    }
+}

+ 90 - 0
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));
+    });
+  });
+});