Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7527: Token Bound Function Oracle AMM

封装 FT 到 NFT 和解封装 NFT 到 FT 的接口,基于嵌入式函数预言机 AMM

Authors Elaine Zhang (@lanyinzly) <lz8aj@virginia.edu>, Jerry <jerrymindflow@gmail.com>, Amandafanny <amandafanny200@gmail.com>, Shouhao Wong (@wangshouh) <wongshouhao@outlook.com>, 0xPoet <0xpoets@gmail.com>
Created 2023-09-03
Discussion Link https://ethereum-magicians.org/t/eip-7527-token-bound-function-oracle-amm-contract/15950
Requires EIP-165, EIP-721

摘要

本提案概述了将 ERC-20 或 ETH 封装为 ERC-721,以及将 ERC-721 解封装为 ERC-20 或 ETH 的接口。函数预言机根据嵌入式函数预言机自动做市商(FOAMM)的公式提供铸造/销毁价格,该做市商执行并清算 NFT 的铸造和销毁。

动机

流动性可能是去中心化系统中的一个重大挑战,特别是对于像 NFT 这样独特或交易较少的代币。为了培育一个无需信任的 NFT 生态系统,函数预言机自动做市商(FOAMM)背后的动机是通过透明的智能合约机制为具有流动性的 NFT 提供自动定价解决方案。

本 ERC 为以下方面提供了创新的解决方案:

  • 自动价格发现
  • 增强流动性

自动价格发现

在 FOAMM 下的交易可以在不需要匹配的交易对手的情况下发生。当直接与资金池交互时,FOAMM 会根据带有预定义功能的预言机自动提供价格。

增强流动性

在传统的 DEX 模型中,流动性由外部参与者提供,这些参与者被称为流动性提供者(LP)。这些 LP 将代币存入流动性池,通过提供流动性来促进交易。这些 LP 的移除或退出会引入显著的波动性,因为它直接影响市场中可用的流动性。

在 FOAMM 系统中,流动性通过 wrapunwrap 在内部添加或移除。FOAMM 减少了对外部 LP 的依赖,并减轻了因其突然退出而引起的波动性风险,因为流动性通过持续的参与者互动不断补充和维护。

规范

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

合约接口:

这里包括三个接口:AgencyAppFactory

AgencyApp 可以由同一个合约实现,也可以分别实现。如果分别实现,则它们应是相互绑定的,并且在初始化后不可升级。

AgencyApp 应该实现 iconstructor 接口来初始化合约中的参数并验证配置参数。如果使用工厂来部署 AgencyApp,则工厂将在部署时自动调用这两个函数。

App 应该实现 onlyAgency() 修饰符,并且 mintburn 应该应用 onlyAgency() 作为修饰符,这限制了只有通过相应的 Agency 调用 MintBurn 才能生效。

Agency 可以选择实现 onlyApp()

Factory 接口是可选的。如果需要重复部署 AgencyApp,则它最有用。

函数预言机通过 getWrapOraclegetUnwrapOracle 实现,它们基于函数中定义的参数和数学公式提供价格。

FOAMM 通过 wrapunwrap 实现,它们调用 getWrapOraclegetUnwrapOracle 以获取 feed 并自动清算。要执行 wrap,FOAMM 接收溢价并在 App 中启动 mint。要执行 unwrap,FOAMM 转移溢价并在 App 中启动 burn

Agency 用作所有 mintburn 传输的单一入口点。

Agency 接口

pragma solidity ^0.8.20;

/**
 * @dev The settings of the agency.
 * @param currency The address of the currency. If `currency` is 0, the currency is Ether.
 * @param basePremium The base premium of the currency.
 * @param feeRecipient The address of the fee recipient.
 * @param mintFeePercent The fee of minting.
 * @param burnFeePercent The fee of burning.
 */

struct Asset {
    address currency;
    uint256 basePremium;
    address feeRecipient;
    uint16 mintFeePercent;
    uint16 burnFeePercent;
}

interface IERC7527Agency {
    /**
     * @dev Allows the account to receive Ether
     *
     * Accounts MUST implement a `receive` function.
     *
     * Accounts MAY perform arbitrary logic to restrict conditions
     * under which Ether can be received.
     */
    receive() external payable;

    /**
     * @dev Emitted when `tokenId` token is wrapped.
     * @param to The address of the recipient of the newly created non-fungible token.
     * @param tokenId The identifier of the newly created non-fungible token.
     * @param premium The premium of wrapping.
     * @param fee The fee of wrapping.
     */
    event Wrap(address indexed to, uint256 indexed tokenId, uint256 premium, uint256 fee);

    /**
     * @dev Emitted when `tokenId` token is unwrapped.
     * @param to The address of the recipient of the currency.
     * @param tokenId The identifier of the non-fungible token to unwrap.
     * @param premium The premium of unwrapping.
     * @param fee The fee of unwrapping.
     */
    event Unwrap(address indexed to, uint256 indexed tokenId, uint256 premium, uint256 fee);

    /**
     * @dev Constructor of the instance contract.
     */
    function iconstructor() external;

    /**
     * @dev Wrap some amount of currency into a non-fungible token.
     * @param to The address of the recipient of the newly created non-fungible token.
     * @param data The data to encode into ifself and the newly created non-fungible token.
     * @return The identifier of the newly created non-fungible token.
     */
    function wrap(address to, bytes calldata data) external payable returns (uint256);

    /**
     * @dev Unwrap a non-fungible token into some amount of currency.
     *
     * Todo: event
     *
     * @param to The address of the recipient of the currency.
     * @param tokenId The identifier of the non-fungible token to unwrap.
     * @param data The data to encode into ifself and the non-fungible token with identifier `tokenId`.
     */
    function unwrap(address to, uint256 tokenId, bytes calldata data) external payable;

    /**
     * @dev Returns the strategy of the agency.
     * @return app The address of the app.
     * @return asset The asset of the agency.
     * @return attributeData The attributeData of the agency.
     */
    function getStrategy() external view returns (address app, Asset memory asset, bytes memory attributeData);

    /**
     * @dev Returns the premium and fee of wrapping.
     * @param data The data to encode to calculate the premium and fee of wrapping.
     * @return premium The premium of wrapping.
     * @return fee The fee of wrapping.
     */
    function getWrapOracle(bytes memory data) external view returns (uint256 premium, uint256 fee);

    /**
     * @dev Returns the premium and fee of unwrapping.
     * @param data The data to encode to calculate the premium and fee of unwrapping.
     * @return premium The premium of wrapping.
     * @return fee The fee of wrapping.
     */
    function getUnwrapOracle(bytes memory data) external view returns (uint256 premium, uint256 fee);

    /**
     * @dev OPTIONAL - This method can be used to improve usability and clarity of Agency, but interfaces and other contracts MUST NOT expect these values to be present.
     * @return the description of the agency, such as how `getWrapOracle()` and `getUnwrapOracle()` are calculated.
     */
    function description() external view returns (string memory);
}

App 接口

ERC7527App 应该从接口 ERC721Metadata 继承 name

pragma solidity ^0.8.20;

interface IERC7527App {
    /**
     * @dev Returns the maximum supply of the non-fungible token.
     */
    function getMaxSupply() external view returns (uint256);

    /**
     * @dev Returns the name of the non-fungible token with identifier `id`.
     * @param id The identifier of the non-fungible token.
     */
    function getName(uint256 id) external view returns (string memory);

    /**
     * @dev Returns the agency of the non-fungible token.
     */
    function getAgency() external view returns (address payable);

    /**
     * @dev Constructor of the instance contract.
     */
    function iconstructor() external;

    /**
     * @dev Sets the agency of the non-fungible token.
     * @param agency The agency of the non-fungible token.
     */
    function setAgency(address payable agency) external;

    /**
     * @dev Mints a non-fungible token to `to`.
     * @param to The address of the recipient of the newly created non-fungible token.
     * @param data The data to encode into the newly created non-fungible token.
     */
    function mint(address to, bytes calldata data) external returns (uint256);

    /**
     * @dev Burns a non-fungible token with identifier `tokenId`.
     * @param tokenId The identifier of the non-fungible token to burn.
     * @param data The data to encode into the non-fungible token with identifier `tokenId`.
     */
    function burn(uint256 tokenId, bytes calldata data) external;
}

Token ID 可以在 mint 函数的 data 参数中指定。

Factory 接口

可选 - 此接口可用于部署 App 和 Agency,但接口和其他合约不应期望存在此接口。

如果需要工厂来部署绑定的 App 和 Agency,则工厂应该实现以下接口:

pragma solidity ^0.8.20;

import {Asset} from "./IERC7527Agency.sol";

/**
 * @dev The settings of the agency.
 * @param implementation The address of the agency implementation.
 * @param asset The parameter of asset of the agency.
 * @param immutableData The immutable data are stored in the code region of the created proxy contract of agencyImplementation.
 * @param initData If init data is not empty, calls proxy contract of agencyImplementation with this data.
 */
struct AgencySettings {
    address payable implementation;
    Asset asset;
    bytes immutableData;
    bytes initData;
}

/**
 * @dev The settings of the app.
 * @param implementation The address of the app implementation.
 * @param immutableData The immutable data are stored in the code region of the created proxy contract of appImplementation.
 * @param initData If init data is not empty, calls proxy contract of appImplementation with this data.
 */
struct AppSettings {
    address implementation;
    bytes immutableData;
    bytes initData;
}

interface IERC7527Factory {
    /**
     * @dev Deploys a new agency and app clone and initializes both.
     * @param agencySettings The settings of the agency.
     * @param appSettings The settings of the app.
     * @param data The data is additional data, it has no specified format and it is sent in call to `factory`.
     * @return appInstance The address of the created proxy contract of appImplementation.
     * @return agencyInstance The address of the created proxy contract of agencyImplementation.
     */
    function deployWrap(AgencySettings calldata agencySettings, AppSettings calldata appSettings, bytes calldata data)
        external
        returns (address, address);
}

理由

之前的接口

ERC-5679 提出了 IERC5679Ext721 接口,用于引入一种一致的方式来扩展 ERC-721 代币标准,以进行铸造和销毁。为了确保向后兼容性,考虑到某些合约未实现 ERC721TokenReceiverIERC7527App 采用 mint 函数而不是 safeMint。为了确保相互绑定的安全性和唯一性,IERC5679Ext721burn 函数的 _from 参数必须是绑定 agency 的合约地址。因此,IERC7527App 中的 burn 函数不包含 _from 参数。

相互绑定

实现 IERC7527AppIERC7527Agency 的合约,以便它们是彼此唯一的owner。wrap 过程是检查收到的可替代代币的溢价金额,然后在 App 中铸造不可替代代币。只有不可替代代币的所有者或批准者才能 unwrap 它。

实现多样性

用户可以在实现 Agency 和 App 接口时自定义函数和费用百分比。

不同的 Agency 实现具有不同的 wrap、unwrap 函数逻辑和不同的 oracleFunction。用户可以自定义货币、初始价格、费用接收地址、费率等,以初始化 Agency 合约。

不同的 App 实现满足不同的用例。用户可以初始化 App 合约。

Factory 不是必需的。 Factory 的实现基于需求。用户可以通过 Factory 选择不同的 Agency 实现和不同的 App 实现来部署自己的合约,将它们组合起来以创建各种产品。

货币类型

IERC7527Agency 中的 currency 是可替代代币的地址。Asset 只能定义一种类型的 currency 作为系统中的可替代代币。currency 支持各种类型的可替代代币,包括 ETH 和 ERC-20

Token id

对于每个 wrap 过程,都应该生成一个唯一的 tokenId。此 tokenId 对于 unwrap 过程中的验证至关重要。它还充当token的独有凭证。此机制确保了合约中资产的安全性。

Wrap 和 Mint

strategy 在实现 Agency 接口时设置,应确保部署后不可升级。

当执行 wrap 函数时,预定的 strategy 参数被传递到 getWrapOracle 函数中,以获取当前的溢价和费用。然后,相应的溢价被转移到 Agency 实例;根据 mintFeePercent,费用被转移到 feeRecipient。随后,App 将 NFT 铸造到用户的地址。

转移到 Agency 中的溢价(代币)不能被移动,除非通过 unwrap 过程。执行 wrap 的行为是 mint 过程的唯一触发器。

Unwrap 和 Burn

当执行 unwrap 函数时,预定的 strategy 参数被传递到 getUnwrapOracle 函数中,以读取当前的溢价和费用。App 销毁 NFT。然后,相应的溢价,减去根据 burnFeePercent 计算的费用,被转移到用户的地址;费用被转移到 feeRecipient。执行 ‘unwrap’ 的行为是 ‘burn’ 过程的唯一触发器。

两个接口一起使用

IERC7527AppIERC7527Agency 可以一起实现以确保安全,但它们可以在初始化之前独立实现以提高灵活性。

定价

getWrapOraclegetUnwrapOracle 用于获取当前的溢价和费用。它们通过预言机函数实现链上价格获取。它们不仅支持在 wrap 和 unwrap 过程中获取溢价和费用,还支持其他合约调用它们以获取溢价和费用,例如借贷合约。

它们可以支持基于链上和链下参数的函数预言机,但建议仅将链上参数用于链上现实的共识。

initDataiconstructor

在 Factory 部署 AppAgency 的过程中,Factory 使用 initData 作为 Calldata 来调用 AgencyApp 合约,并调用 AppAgency 中的 iconstructor 函数。

initData 主要用于调用参数化的初始化函数,而 iconstructor 通常用于验证配置参数和非参数化的初始化函数。

向后兼容性

未发现向后兼容性问题。

参考实现

pragma solidity ^0.8.20;

import {
    ERC721Enumerable,
    ERC721,
    IERC721Enumerable
} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol";
import {IERC7527App} from "./interfaces/IERC7527App.sol";
import {IERC7527Agency, Asset} from "./interfaces/IERC7527Agency.sol";
import {IERC7527Factory, AgencySettings, AppSettings} from "./interfaces/IERC7527Factory.sol";

contract ERC7527Agency is IERC7527Agency {
    using Address for address payable;

    receive() external payable {}

    function iconstructor() external override pure {
        (, Asset memory _asset,) = getStrategy();
        require(_asset.basePremium != 0, "LnModule: zero basePremium");
    }

    function unwrap(address to, uint256 tokenId, bytes calldata data) external payable override {
        (address _app, Asset memory _asset,) = getStrategy();
        require(_isApprovedOrOwner(_app, msg.sender, tokenId), "LnModule: not owner");
        IERC7527App(_app).burn(tokenId, data);
        uint256 _sold = IERC721Enumerable(_app).totalSupply();
        (uint256 premium, uint256 burnFee) = getUnwrapOracle(abi.encode(_sold));
        _transfer(address(0), payable(to), premium - burnFee);
        _transfer(address(0), _asset.feeRecipient, burnFee);
        emit Unwrap(to, tokenId, premium, burnFee);
    }

    function wrap(address to, bytes calldata data) external payable override returns (uint256) {
        (address _app, Asset memory _asset,) = getStrategy();
        uint256 _sold = IERC721Enumerable(_app).totalSupply();
        (uint256 premium, uint256 mintFee) = getWrapOracle(abi.encode(_sold));
        require(msg.value >= premium + mintFee, "ERC7527Agency: insufficient funds");
        _transfer(address(0), _asset.feeRecipient, mintFee);
        if (msg.value > premium + mintFee) {
            _transfer(address(0), payable(msg.sender), msg.value - premium - mintFee);
        }
        uint256 id_ = IERC7527App(_app).mint(to, data);
        require(_sold + 1 == IERC721Enumerable(_app).totalSupply(), "ERC7527Agency: Reentrancy");
        emit Wrap(to, id_, premium, mintFee);
        return id_;
    }

    function getStrategy() public pure override returns (address app, Asset memory asset, bytes memory attributeData) {
        uint256 offset = _getImmutableArgsOffset();
        address currency;
        uint256 basePremium;
        address payable feeRecipient;
        uint16 mintFeePercent;
        uint16 burnFeePercent;
        assembly {
            app := shr(0x60, calldataload(add(offset, 0)))
            currency := shr(0x60, calldataload(add(offset, 20)))
            basePremium := calldataload(add(offset, 40))
            feeRecipient := shr(0x60, calldataload(add(offset, 72)))
            mintFeePercent := shr(0xf0, calldataload(add(offset, 92)))
            burnFeePercent := shr(0xf0, calldataload(add(offset, 94)))
        }
        asset = Asset(currency, basePremium, feeRecipient, mintFeePercent, burnFeePercent);
        attributeData = "";
    }

    function getUnwrapOracle(bytes memory data) public pure override returns (uint256 premium, uint256 fee) {
        uint256 input = abi.decode(data, (uint256));
        (, Asset memory _asset,) = getStrategy();
        premium = _asset.basePremium + input * _asset.basePremium / 100;
        fee = premium * _asset.burnFeePercent / 10000;
    }

    function getWrapOracle(bytes memory data) public pure override returns (uint256 premium, uint256 fee) {
        uint256 input = abi.decode(data, (uint256));
        (, Asset memory _asset,) = getStrategy();
        premium = _asset.basePremium + input * _asset.basePremium / 100;
        fee = premium * _asset.mintFeePercent / 10000;
    }

    function _transfer(address currency, address recipient, uint256 premium) internal {
        if (currency == address(0)) {
            payable(recipient).sendValue(premium);
        } else {
            IERC20(currency).transfer(recipient, premium);
        }
    }

    function _isApprovedOrOwner(address app, address spender, uint256 tokenId) internal view virtual returns (bool) {
        IERC721Enumerable _app = IERC721Enumerable(app);
        address _owner = _app.ownerOf(tokenId);
        return (spender == _owner || _app.isApprovedForAll(_owner, spender) || _app.getApproved(tokenId) == spender);
    }
    /// @return offset The offset of the packed immutable args in calldata
    //返回 calldata 中打包的不可变参数的偏移量

    function _getImmutableArgsOffset() internal pure returns (uint256 offset) {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            offset := sub(calldatasize(), add(shr(240, calldataload(sub(calldatasize(), 2))), 2))
        }
    }
}

contract ERC7527App is ERC721Enumerable, IERC7527App {
    constructor() ERC721("ERC7527App", "EA") {}

    address payable private _oracle;

    modifier onlyAgency() {
        require(msg.sender == _getAgency(), "only agency");
        _;
    }

    function iconstructor() external {}

    function getName(uint256) external pure returns (string memory) {
        return "App";
    }

    function getMaxSupply() public pure override returns (uint256) {
        return 100;
    }

    function getAgency() external view override returns (address payable) {
        return _getAgency();
    }

    function setAgency(address payable oracle) external override {
        require(_getAgency() == address(0), "already set");
        _oracle = oracle;
    }

    function mint(address to, bytes calldata data) external override onlyAgency returns (uint256 tokenId) {
        require(totalSupply() < getMaxSupply(), "max supply reached");
        tokenId = abi.decode(data, (uint256));
        _mint(to, tokenId);
    }

    function burn(uint256 tokenId, bytes calldata) external override onlyAgency {
        _burn(tokenId);
    }

    function _getAgency() internal view returns (address payable) {
        return _oracle;
    }
}

contract ERC7527Factory is IERC7527Factory {
    using ClonesWithImmutableArgs for address;

    function deployWrap(AgencySettings calldata agencySettings, AppSettings calldata appSettings, bytes calldata)
        external
        override
        returns (address appInstance, address agencyInstance)
    {
        appInstance = appSettings.implementation.clone(appSettings.immutableData);
        {
            agencyInstance = address(agencySettings.implementation).clone(
                abi.encodePacked(
                    appInstance,
                    agencySettings.asset.currency,
                    agencySettings.asset.basePremium,
                    agencySettings.asset.feeRecipient,
                    agencySettings.asset.mintFeePercent,
                    agencySettings.asset.burnFeePercent,
                    agencySettings.immutableData
                )
            );
        }

        IERC7527App(appInstance).setAgency(payable(agencyInstance));

        IERC7527Agency(payable(agencyInstance)).iconstructor();
        IERC7527App(appInstance).iconstructor();

        if (agencySettings.initData.length != 0) {
            (bool success, bytes memory result) = agencyInstance.call(agencySettings.initData);

            if (!success) {
                assembly {
                    revert(add(result, 32), mload(result))
                }
            }
        }

        if (appSettings.initData.length != 0) {
            (bool success, bytes memory result) = appInstance.call(appSettings.initData);

            if (!success) {
                assembly {
                    revert(add(result, 32), mload(result))
                }
            }
        }
    }
}

安全考虑

欺诈预防

考虑以下事项以确保合约的安全性:

  • 检查修饰符 onlyAgency()onlyApp() 是否已正确实现并应用。

  • 检查函数策略。

  • 检查合约是否容易受到重入攻击。

  • 检查是否所有不可替代代币都可以使用从 FOAMM 计算的溢价进行 unwrap。

版权

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

Citation

Please cite this document as:

Elaine Zhang (@lanyinzly) <lz8aj@virginia.edu>, Jerry <jerrymindflow@gmail.com>, Amandafanny <amandafanny200@gmail.com>, Shouhao Wong (@wangshouh) <wongshouhao@outlook.com>, 0xPoet <0xpoets@gmail.com>, "ERC-7527: Token Bound Function Oracle AMM [DRAFT]," Ethereum Improvement Proposals, no. 7527, September 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7527.