Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7527: Token Bound Function Oracle AMM

Interfaces that wrap FT to NFT and unwrap NFT to FT based on an embedded Function Oracle 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

Abstract

This proposal outlines interfaces for wrapping ERC-20 or ETH to ERC-721 and unwrap ERC-721 to ERC-20 or ETH. A function oracle feeds mint/burn prices based on an embedded equation of Function Oracle Automated Market Maker(FOAMM), which executes and clears the mint and burn of NFT.

Motivation

Liquidity can be a significant challenge in decentralized systems, especially for unique or less commonly traded tokens like NFTs. To foster a trustless NFT ecosystem, the motivation behind Function Oracle Automated Market Maker(FOAMM) is to provide automated pricing solutions for NFTs with liquidity through transparent, smart contract mechanisms.

This ERC provides innovative solutions for the following aspects:

  • Automated Price Discovery
  • Liquidity Enhancement

Automated Price Discovery

Transactions under FOAMM can occur without the need for a matching counterparty. When interacting directly with the pool, FOAMM automatically feeds prices based on the oracle with predefined function.

Liquidity Enhancement

In traditional DEX models, liquidity is supplied by external parties, known as Liquidity Providers(LP). These LPs deposit tokens into liquidity pools, facilitating exchanges by providing the liquidity. The removal or withdrawal of these LPs can introduce significant volatility, as it directly impacts the available liquidity in the market.

In a FOAMM system, the liquidity is added or removed internally through wrap or unwrap. FOAMM reduces reliance on external LPs and mitigates the risk of volatility caused by their sudden withdrawal, as the liquidity is continuously replenished and maintained through ongoing participant interactions.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Contract Interfaces:

Three interfaces are included here: Agency, App, and Factory.

Agency and App MAY be implemented by the same contract or MAY be separately implemented. If separately implemented, they SHALL be mutually bounded and not upgradable after initialization.

Agency and App should implement iconstructor interface to initialize the parameters within the contract and validate the configuration parameters. If factory is used to deploy Agency and App, factory will automatically call the two functions when deploying.

App SHALL implement onlyAgency() modifier and mint and burn SHALL apply onlyAgency() as a modifier, which restricts calls to Mint and Burn only have effect if they are called through the corresponding Agency.

Agency is OPTIONAL to implement onlyApp().

The Factory interface is OPTIONAL. It is most useful if Agency and App need to be deployed repeatedly.

Function Oracle is implemented through getWrapOracle and getUnwrapOracle, which feeds prices based on parameters and mathematical equations defined in the functions.

FOAMM is implemented through wrap and unwrap, which calls getWrapOracle and getUnwrapOracle to get the feed and automatically clears. To perform wrap, FOAMM receives the premium and initiate mint in App. To perform unwrap, FOAMM transfer the premium and initiate burn in App.

Agency serves as a single entry point for all mint and burn transfer.

Agency Interface

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() public view returns (string);
}

App Interface

ERC7527App SHALL inherit name from interface ERC721Metadata.

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 can be specified in data parameter of mint function.

Factory Interface

OPTIONAL - This interface can be used to deploy App and Agency, but interfaces and other contracts MUST NOT expect this interface to be present.

If a factory is needed to deploy bounded App and Agency, the factory SHALL implement the following interface:

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);
}

Rationale

Prior Interfaces

ERC-5679 proposed IERC5679Ext721 interface for introducing a consistent way to extend ERC-721 token standards for minting and burning. To ensure the backward compatibility, considering some contracts which do not implement ERC721TokenReceiver, IERC7527App employ mint function instead of safeMint. To ensure the safety and the uniqueness of mutual bound, the _from parameter of the burn function in IERC5679Ext721 must be the contract address of the bounded agency. Thus, burn function in IERC7527App does not contain the _from parameter.

Mutual Bound

Implement contracts for IERC7527App and IERC7527Agency so that they are each other’s only owner. The wrap process is to check the premium amount of the fungible token received and then mint non-fungible token in the App. Only the owner or an approver of the non-fungible token can unwrap it.

Implementation Diversity

Users can customize function and fee percentage when implement the Agency and the App interfaces.

Different Agency implementations have distinct wrap, unwrap function logic, and different oracleFunction. Users can customize the currency, initial price, fee receiving address, fee rate, etc., to initialize the Agency contract.

Different App implementations cater to various use cases. Users can initialize the App contract.

Factory is not required. Factory implementation is need-based. Users can deploy their own contracts by selecting different Agency implementations and different App implementations through the Factory, combining them to create various products.

Currency types

currency in IERC7527Agency is the address of fungible token. Asset can only define one type of currency as the fungible token in the system. currency supports various kinds of fungible tokens including ETH and ERC-20.

Token id

For each wrap process, a unique tokenId should be generated. This tokenId is essential for verification during the unwrap process. It also serves as the exclusive credential for the token. This mechanism ensures the security of assets in contracts.

Wrap and Mint

The strategy is set while implementing the Agency interface, and it should be ensured not upgradable once deployed.

When executing the wrap function, the predetermined strategy parameters are passed into the getWrapOracle function to fetch the current premium and fee. The respective premium is then transferred to the Agency instance; the fee, according to mintFeePercent is transferred to feeRecipient. Subsequently, the App mints the NFT to the user’s address.

Premium(tokens) transferred into the Agency cannot be moved, except through the unwrap process. The act of executing wrap is the sole trigger for the mint process.

Unwrap and Burn

When executing the unwrap function, predetermined strategy parameters are passed into the getUnwrapOracle function to read the current premium and fee. The App burns the NFT. Then, the corresponding premium, subtracting the fee according to burnFeePercent, is then transferred to the user’s address; the fee is transferred to feeRecipient. The act of executing ‘unwrap’ is the sole trigger for the ‘burn’ process.

Two interfaces use together

IERC7527App and IERC7527Agency can be implemented together for safety, but they can be independently implemented before initialization for flexibiliy.

Pricing

getWrapOracle and getUnwrapOracle are used to fetch the current premium and fee. They implement on-chain price fetching through oracle functions. They not only support fetching the premium and fee during the wrap and unwrap processes but also support other contracts calling them to obtain the premium and fee, such as lending contracts.

They can support function oracle based on on-chain and off-chain parameters, but on-chain parameters are suggested only for consensus of on-chain reality.

initData and iconstructor

During the deployment of App and Agency by the Factory, the Factory uses initData as Calldata to call the Agency and App contracts and also invokes the iconstructor functions within App and Agency.

The initData is mainly used to call the parameterized initialization functions, while iconstructor is often used to validate configuration parameters and non-parameterized initialization functions.

Backwards Compatibility

No backward compatibility issues found.

Reference Implementation

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

    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))
                }
            }
        }
    }
}

Security Considerations

Fraud Prevention

Consider the following for the safety of the contracts:

  • Check whether modifiers onlyAgency() and onlyApp() are proporly implemented and applied.

  • Check the function strategies.

  • Check whether the contracts can be subject to re-entrancy attack.

  • Check whether all non-fungible tokens can be unwrapped with the premium calculated from FOAMM.

Copyright and related rights waived via 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.