Token Bound Function Oracle AMM
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()
andonlyApp()
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
Copyright and related rights waived via CC0.