ERC-3475 Abstract Storage Bonds
Final · Tokens · created 2021-04-05
A standard that tokenizes obligations with redemption conditions, like bonds, using a two-level ID of class and nonce, and lets redemption conditions and metadata be stored directly onchain in a free-form structure.
Why it is needed
Existing token standards cannot represent bonds that carry multiple redemption conditions. For example, the LP token (Liquidity Provider token) received for providing liquidity to a decentralized exchange is a plain ERC-20 token, so it cannot hold complex reward and redemption rules onchain. Creating bonds with different terms for each issuance time requires deploying a separate contract each time, and external services such as wallets and exchanges cannot read the balance and redemption conditions of each bond in a consistent way. There was also no standardized way to bundle several bonds and move them in one step, or to split them up and trade them on a secondary market. This standard solves these problems by providing multiple token IDs, free-form onchain metadata, and batch processing in a single interface.
What to implement
function transferFrom(address _from, address _to, Transaction[] calldata _transactions) external;: Provided that isApprovedFor approval exists, moves the bonds in the specified class, nonce, and amount bundles (the Transaction array) from _from to _to.function transferAllowanceFrom(address _from,address _to, Transaction[] calldata _transactions) public ;: Moves the bonds from _from to _to only within the amount range permitted by allowance.function issue(address _to, Transaction[] calldata _transaction) external;: A single issuing entity issues bonds of the specified class, nonce, and amount to the _to address.function redeem(address _from, Transaction[] calldata _transactions) external;: Redeems the bonds of the _from address after the maturity conditions are met. Calls must be restricted to the issuer contract only.function burn(address _from, Transaction[] calldata _transactions) external;: Voids the bonds (equivalent to sending them to address(0)). Only the controller contract may call it.function approve(address _spender, Transaction[] calldata _transactions) external;: Allows _spender to use the caller's bonds of a specific class and nonce up to the specified amount. Calling it again overwrites the existing allowance.function setApprovalFor(address _operator, bool _approved) external returns(bool approved);: Grants or revokes operator permission for _operator to manage all of the caller's bonds.function totalSupply(uint256 classId, uint256 nonceId) external view returns (uint256);: Returns the total supply of bonds for the given class and nonce. It includes burned and redeemed amounts.function redeemedSupply(uint256 classId, uint256 nonceId) external view returns (uint256);: Returns the amount of bonds already redeemed for the given class and nonce.function activeSupply(uint256 classId, uint256 nonceId) external view returns (uint256);: Returns the amount of bonds that are still live and not yet redeemed.function burnedSupply(uint256 classId, uint256 nonceId) external view returns (uint256);: Returns the amount of bonds burned for the given class and nonce.function balanceOf(address _account, uint256 classId, uint256 nonceId) external view returns (uint256);: Returns the balance of bonds of the given class and nonce held by a specific address. Redeemed bonds are included.function classMetadata(uint256 metadataId) external view returns (Metadata memory);: Returns the JSON structure of the class metadata (title, type, description).function nonceMetadata(uint256 classId, uint256 metadataId) external view returns (Metadata memory);: Returns the JSON structure of the nonce metadata belonging to a specific class.function classValues(uint256 classId, uint256 metadataId) external view returns (Values memory);: Lets anyone read the actual values (strings, numbers, addresses, and so on) stored in the metadata entries of the given class.function nonceValues(uint256 classId, uint256 nonceId, uint256 metadataId) external view returns (Values memory);: Returns the actual values stored in the metadata entries of the bond with the given class and nonce.function getProgress(uint256 classId, uint256 nonceId) external view returns (uint256 progressAchieved, uint256 progressRemaining);: Returns the current progress toward the bond's maturity and the amount of progress remaining. The basis can be time or a liquidity ratio.function allowance(address _owner, address _spender, uint256 classId, uint256 nonceId) external returns(uint256);: Returns the remaining usable amount of the given class and nonce bonds that _owner has allowed _spender to use.function isApprovedFor(address _owner, address _operator) external view returns (bool);: Returns whether _operator has operator permission to manage _owner's bonds.event Issue(address indexed _operator, address indexed _to, Transaction[] _transactions);: An event that must be emitted whenever bonds are issued. It must not include issuances with an amount of 0.event Redeem(address indexed _operator, address indexed _from, Transaction[] _transactions);: An event that must be emitted whenever bonds are redeemed. It must not include redemptions with an amount of 0.event burn(address _operator, address _owner, Transaction[] _transactions);: An event that must be emitted when the bank contract burns or voids bonds.event Transfer(address indexed _operator, address indexed _from, address indexed _to, Transaction[] _transactions);: An event that must be emitted whenever bonds are transferred. It is not used for transfers where _from is 0x0; the Issue event is used instead.event ApprovalFor(address indexed _owner, address indexed _operator, bool _approved);: An event emitted when an owner grants or revokes an operator's permission to transfer bonds.
In depth
The two-level ID structure of class and nonce
Within a single contract, a bond is identified by two values, classId and nonceId. The class represents the type of bond, and the nonce represents differences in issuance time or issuance terms within the same class. Nonces in the same class share the class metadata, but nonces are not fungible with each other, and each nonce can have its own metadata, supply, and redemption conditions. When a bond is transferred, all of its metadata passes to the new owner.
The Transaction struct and batch processing
Every state-changing function takes an array of Transaction structs. The struct has three fields, uint256 classId, uint256 nonceId, and uint256 _amount, which denote the bond class, the nonce that distinguishes issuance terms, and the amount. This allows several bonds of different classes and nonces to be issued, transferred, redeemed, burned, or approved in one transaction. Once execution finishes, the new owner has the right to claim the face value of each bond, and this bundled processing is useful for uses such as secondary market trading.
Permission rules for issuing, redeeming, and burning
issue() must be performed by a single entity (MUST), and the example given is a role-based ownership contract linked to a collateral liquidity pool. redeem() must restrict the call to the bond issuer contract and must run only after the maturity conditions (end time, total active liquidity, and so on) are met (MUST), and only the bank or a secondary market maker contract should call it (SHOULD). burn() must be called only by the controller contract (MUST) and likewise runs only after the maturity conditions are met. Here, the bank is the entity that issues, redeems, and burns bonds after receiving liquidity, and usually means a single entity with administrator authority over the pool.
Two approval schemes
approve() gives _spender the right to use a specific (classId, nonceId) bond up to the specified amount, and calling it again overwrites the existing allowance. setApprovalFor() turns on or off an operator's permission to manage all of the caller's bond classes. transferFrom() requires an isApprovedFor(_from, _to, classId) approval, and transferAllowanceFrom() requires allowance(_from, msg.sender, classId, nonceId) for each Transaction entry. Both approval functions must be callable only by the bank or the account owner.
Supply accounting and maturity progress lookup
Supply is counted in four ways. totalSupply() is the total supply including burned and redeemed amounts, activeSupply() is the active supply that has not been redeemed, redeemedSupply() is the redeemed amount, and burnedSupply() is the burned amount. balanceOf() returns the balance of a specific address, including redeemed bonds. getProgress() returns two values, progressAchieved and progressRemaining, to show the progress toward maturity. The basis may differ by implementation, such as a liquidity ratio or time, and the redemption conditions should be defined in an internal function (SHOULD).
Abstract storage of metadata onchain
Metadata defines its schema with a Metadata struct holding three strings, title, _type, and description, and the actual values are held in a Values struct with the fields stringValue, uintValue, addressValue, boolValue, and bytesValue. The schema is read with classMetadata() and nonceMetadata(), and the corresponding values are read by passing an index key to classValues() and nonceValues(). For a symbol entry, for example, the class ID is used as the key to find that class's symbol string in the values array. The standard's name (abstract storage) comes from the fact that it can hold every specification without external storage while not forcing a fixed storage pattern.
Events and compatibility with existing standards
The Issue, Redeem, Transfer, burn, and ApprovalFor events must be emitted for each corresponding transaction so that the bond lifecycle (issuance, redemption, transfer) can be indexed. Operations with an amount of 0 must not be included in events, and for transfers where _from is 0x0, the Issue event must be used instead of Transfer. This standard's interface is compatible with the EIP-20, EIP-721, and EIP-1155 interfaces, and an existing EIP-20 token can also issue bonds by delegating its minting authority to a bank contract that embeds this standard. Collateral can be a fungible token such as EIP-20, a non-fungible token such as EIP-721 and EIP-1155, or another bond built with this standard. A separate bank contract is recommended for lookups and future upgrades.
AMM liquidity pool consolidation
Early automated market makers (AMMs) managed liquidity with a separate contract and an EIP-20 LP token for each token pair, which reduced the liquidity gathered in any one pool and caused unnecessary gas costs and slippage. With this standard, the liquidity data structure that maps to class and nonce makes it possible to build one large liquidity pool containing every pair, and a bond holder's liquidity can be expressed as the ratio of the token pair within the pool. The effect is that EIP-20 LP tokens, which used to be deployed separately for each pool factory, are aggregated into a single bond and consolidated into one pool.
Things to watch out for
- setApprovalFor hands over full authority: setApprovalFor(_operatorAddress) grants the operator role, and by default the operator gets every permission over the bonds, including transfer, burn, and redemption. Use it with care.
- Use approve for one-time allowances: To grant a one-time right to use a specific bond (classId, nonceId), do not approve all classes with setApprovalFor. Instead, specify the Transaction array to allow and call approve().