Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-6682: NFT 闪电贷

用于 ERC-721 NFT 闪电贷的最小接口

Authors out.eth (@outdoteth)
Created 2023-03-12
Discussion Link https://ethereum-magicians.org/t/eip-6682-nft-flashloans/13294
Requires EIP-20, EIP-721, EIP-3156

摘要

此标准是现有闪电贷标准(ERC-3156)的扩展,旨在支持 ERC-721 NFT 闪电贷。它提出了一种让闪电贷提供者将 NFT 借给合约的方法,条件是在同一笔交易中偿还贷款并支付一些费用。

动机

当前的闪电贷标准 ERC-3156 仅支持 ERC-20 代币。ERC-721 代币与 ERC-20 代币有很大不同,需要扩展现有标准才能支持它们。

NFT 闪电贷在任何检查 NFT 所有权的操作中都可能很有用。例如,领取空投、领取质押奖励或采取游戏内操作,例如领取耕种的资源。

规范

本文档中的关键词“必须”、“不得”、“必需”、“应该”、“不应该”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

合约接口

pragma solidity ^0.8.19;

interface IERC6682 {
    /// @dev 用于支付闪电贷费用的代币地址。
    function flashFeeToken() external view returns (address);

    /// @dev NFT 是否可用于闪电贷。
    /// @param token NFT 合约的地址。
    /// @param tokenId NFT 的 ID。
    function availableForFlashLoan(address token, uint256 tokenId) external view returns (bool);
}

flashFeeToken 函数必须返回用于支付闪电贷费用的代币地址。

如果用于支付闪电贷费用的代币是 ETH,则 flashFeeToken 必须返回 address(0)

availableForFlashLoan 函数必须返回 tokentokenId 是否可用于闪电贷。如果 tokenId 当前不可用于闪电贷,则 availableForFlashLoan 必须返回 false 而不是 revert。

实现者 MUST 还需要实现 IERC3156FlashLender

理由

上述修改是对现有闪电贷标准进行的最简单的补充,旨在支持 NFT。

我们选择尽可能多地扩展现有的闪电贷标准(ERC-3156),而不是创建一个全新的标准,因为闪电贷标准已经被广泛采用,并且只需要进行少量更改即可支持 NFT。

在大多数情况下,费用支付的处理方式最好是使用与贷款 NFT 不同的货币支付,因为 NFT 本身并不总是可以分割的。考虑以下示例,其中闪电贷提供者对每个闪电贷的 NFT 收取 0.1 ETH 的费用;该接口必须提供允许借款人确定每个 NFT 的费率以及应支付费用的货币的方法。

向后兼容性

此 EIP 与 ERC-3156 完全向后兼容,但 maxFlashLoan 方法除外。此方法在 NFT 的上下文中没有意义,因为 NFT 不是同质化的。但是,它是现有闪电贷标准的一部分,因此如果不破坏向后兼容性,则无法删除它。建议任何实施此 EIP 但不打算支持 ERC-20 闪电贷的合约应始终从 maxFlashLoan 返回 11 反映了每次 flashLoan 调用只能闪电贷一个 NFT 的事实。例如:

function maxFlashLoan(address token) public pure override returns (uint256) {
    // if a contract also supports flash loans for ERC20 tokens then it can
    // 如果合约还支持 ERC20 代币的闪电贷,则它可以
    // return some value here instead of 1
    // 在这里返回一些值而不是 1
    return 1;
}

参考实现

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.19;

import "../interfaces/IERC20.sol";
import "../interfaces/IERC721.sol";
import "../interfaces/IERC3156FlashBorrower.sol";
import "../interfaces/IERC3156FlashLender.sol";
import "../interfaces/IERC6682.sol";

contract ExampleFlashLender is IERC6682, IERC3156FlashLender {
    uint256 internal _feePerNFT;
    address internal _flashFeeToken;

    constructor(uint256 feePerNFT_, address flashFeeToken_) {
        _feePerNFT = feePerNFT_;
        _flashFeeToken = flashFeeToken_;
    }

    function flashFeeToken() public view returns (address) {
        return _flashFeeToken;
    }

    function availableForFlashLoan(address token, uint256 tokenId) public view returns (bool) {
        // return if the NFT is owned by this contract
        // 如果 NFT 由此合约拥有,则返回
        try IERC721(token).ownerOf(tokenId) returns (address result) {
            return result == address(this);
        } catch {
            return false;
        }
    }

    function flashFee(address token, uint256 tokenId) public view returns (uint256) {
        return _feePerNFT;
    }

    function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 tokenId, bytes calldata data)
        public
        returns (bool)
    {
        // check that the NFT is available for a flash loan
        // 检查 NFT 是否可用于闪电贷
        require(availableForFlashLoan(token, tokenId), "IERC6682: NFT not available for flash loan");

        // transfer the NFT to the borrower
        // 将 NFT 转移给借款人
        IERC721(token).safeTransferFrom(address(this), address(receiver), tokenId);

        // calculate the fee
        // 计算费用
        uint256 fee = flashFee(token, tokenId);

        // call the borrower
        // 调用借款人
        bool success =
            receiver.onFlashLoan(msg.sender, token, tokenId, fee, data) == keccak256("ERC3156FlashBorrower.onFlashLoan");

        // check that flashloan was successful
        // 检查闪电贷是否成功
        require(success, "IERC6682: Flash loan failed");
        
        // check that the NFT was returned by the borrower
        // 检查 NFT 是否已由借款人退回
        require(IERC721(token).ownerOf(tokenId) == address(this), "IERC6682: NFT not returned by borrower");

        // transfer the fee from the borrower
        // 从借款人处转移费用
        IERC20(flashFeeToken()).transferFrom(msg.sender, address(this), fee);

        return success;
    }

    function maxFlashLoan(address token) public pure override returns (uint256) {
        // if a contract also supports flash loans for ERC20 tokens then it can
        // 如果合约还支持 ERC20 代币的闪电贷,则它可以
        // return some value here instead of 1
        // 在这里返回一些值而不是 1
        return 1;
    }

    function onERC721Received(address, address, uint256, bytes memory) public returns (bytes4) {
        return this.onERC721Received.selector;
    }
}

安全注意事项

flashFeeToken 方法可能会返回恶意合约。打算调用从 flashFeeToken 方法返回的地址的借款人应注意确保该合约不是恶意的。他们可以这样做的一种方法是验证从 flashFeeToken 返回的地址是否与用户输入匹配。

需要讨论。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

out.eth (@outdoteth), "ERC-6682: NFT 闪电贷 [DRAFT]," Ethereum Improvement Proposals, no. 6682, March 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6682.