Alert Source Discuss
Standards Track: ERC

ERC-3475: Abstract Storage Bonds

Interface for creating tokenized obligations with abstract on-chain metadata storage

Authors Yu Liu (@yuliu-debond), Varun Deshpande (@dr-chain), Cedric Ngakam (@drikssy), Dhruv Malik (@dhruvmalik007), Samuel Gwlanold Edoumou (@Edoumou), Toufic Batrice (@toufic0710)
Created 2021-04-05
Requires EIP-20, EIP-721, EIP-1155

Abstract

  • This EIP allows the creation of tokenized obligations with abstract on-chain metadata storage. Issuing bonds with multiple redemption data cannot be achieved with existing token standards.

  • This EIP enables each bond class ID to represent a new configurable token type and corresponding to each class, corresponding bond nonces to represent an issuing condition or any other form of data in uint256. Every single nonce of a bond class can have its metadata, supply, and other redemption conditions.

  • Bonds created by this EIP can also be batched for issuance/redemption conditions for efficiency on gas costs and UX side. And finally, bonds created from this standard can be divided and exchanged in a secondary market.

Motivation

Current LP (Liquidity Provider) tokens are simple EIP-20 tokens with no complex data structure. To allow more complex reward and redemption logic to be stored on-chain, we need a new token standard that:

  • Supports multiple token IDs
  • Can store on-chain metadata
  • Doesn’t require a fixed storage pattern
  • Is gas-efficient.

Also Some benefits:

  • This EIP allows the creation of any obligation with the same interface.
  • It will enable any 3rd party wallet applications or exchanges to read these tokens’ balance and redemption conditions.
  • These bonds can also be batched as tradeable instruments. Those instruments can then be divided and exchanged in secondary markets.

Specification

Definition

Bank: an entity that issues, redeems, or burns bonds after getting the necessary amount of liquidity. Generally, a single entity with admin access to the pool.

Functions

pragma solidity ^0.8.0;

/**
* transferFrom
* @param _from argument is the address of the bond holder whose balance is about to decrease.
* @param _to argument is the address of the bond recipient whose balance is about to increase.
* @param _transactions is the `Transaction[] calldata` (of type ['classId', 'nonceId', '_amountBonds']) structure defined in the rationale section below.
* @dev transferFrom MUST have the `isApprovedFor(_from, _to, _transactions[i].classId)` approval to transfer `_from` address to `_to` address for given classId (i.e for Transaction tuple corresponding to all nonces).
e.g:
* function transferFrom(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B, [IERC3475.Transaction(1,14,500)]);
* transfer from `_from` address, to `_to` address, `500000000` bonds of type class`1` and nonce `42`.
*/

function transferFrom(address _from, address _to, Transaction[] calldata _transactions) external;

/**
* transferAllowanceFrom
* @dev allows the transfer of only those bond types and nonces being allotted to the _to address using allowance().
* @param _from is the address of the holder whose balance is about to decrease.
* @param _to is the address of the recipient whose balance is about to increase.
* @param _transactions is the `Transaction[] calldata` structure defined in the section `rationale` below.
* @dev transferAllowanceFrom MUST have the `allowance(_from, msg.sender, _transactions[i].classId, _transactions[i].nonceId)` (where `i` looping for [ 0 ...Transaction.length - 1] ) 
e.g:
* function transferAllowanceFrom(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B, [IERC3475.Transaction(1,14,500)]);
* transfer from `_from` address, to `_to` address, `500000000` bonds of type class`1` and nonce `42`.
*/

function transferAllowanceFrom(address _from,address _to, Transaction[] calldata _transactions) public ;

/**
* issue 
* @dev allows issuing any number of bond types (defined by values in Transaction tuple as param) to an address.
* @dev it MUST be issued by a single entity (for instance, a role-based ownable contract that has integration with the liquidity pool of the deposited collateral by `_to` address).
* @param `_to` argument is the address to which the bond will be issued.
* @param `_transactions` is the `Transaction[] calldata` (ie array of issued bond class, bond nonce and amount of bonds to be issued).
* @dev transferAllowanceFrom MUST have the `allowance(_from, msg.sender, _transactions[i].classId, _transactions[i].nonceId)` (where `i` looping for [ 0 ...Transaction.length - 1] ) 
e.g:
example: issue(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]);
issues `1000` bonds with a class of `0` to address `0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef` with a nonce of `5`.
*/
function issue(address _to, Transaction[] calldata _transaction) external; 

/**
* redeem
* @dev permits redemption of bond from an address.
* @dev the calling of this function needs to be restricted to the bond issuer contract.
* @param `_from` is the address from which the bond will be redeemed.
* @param `_transactions` is the `Transaction[] calldata` structure (i.e., array of tuples with the pairs of (class, nonce and amount) of the bonds that are to be redeemed). Further defined in the rationale section.
* @dev redeem function for a given class, and nonce category MUST BE done after certain conditions for maturity (can be end time, total active liquidity, etc.) are met. 
* @dev furthermore, it SHOULD ONLY be called by the bank or secondary market maker contract.
e.g:
* redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, [IERC3475.Transaction(1,14,500)]);
means “redeem from wallet address(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef), 500000000 of bond class1 and nonce 42.
*/

function redeem(address _from, Transaction[] calldata _transactions) external; 

/**
* burn
* @dev permits nullifying of the bonds (or transferring given bonds to address(0)).
* @dev burn function for given class and nonce MUST BE called by only the controller contract.
* @param _from is the address of the holder whose bonds are about to burn.
* @param `_transactions` is the `Transaction[] calldata` structure (i.e., array of tuple with the pairs of (class, nonce and amount) of the bonds that are to be burned). further defined in the rationale.
* @dev burn function for a given class, and nonce category MUST BE done only after certain conditions for maturity (can be end time, total active liquidity, etc). 
* @dev furthermore, it SHOULD ONLY be called by the bank or secondary market maker contract.
* e.g:  
* burn(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[IERC3475.Transaction(1,14,500)]);
* means burning 500000000 bonds of class 1 nonce 42 owned by address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B.
*/
function burn(address _from, Transaction[] calldata _transactions) external; 

/**
* approve
* @dev Allows `_spender` to withdraw from the msg.sender the bonds of `_amount` and type (classId and nonceId).
* @dev If this function is called again, it overwrites the current allowance with the amount.
* @dev `approve()` should only be callable by the bank, or the owner of the account.
* @param `_spender` argument is the address of the user who is approved to transfer the bonds.
* @param `_transactions` is the `Transaction[] calldata` structure (ie array of tuple with the pairs of (class,nonce, and amount) of the bonds that are to be approved to be spend by _spender). Further defined in the rationale section.
* e.g: 
* approve(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[IERC3475.Transaction(1,14,500)]);
* means owner of address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B is approved to manage 500 bonds from class 1 and Nonce 14.
*/

function approve(address _spender, Transaction[] calldata _transactions) external;

/**
* SetApprovalFor
* @dev enable or disable approval for a third party (“operator”) to manage all the Bonds in the given class of the caller’s bonds.
* @dev If this function is called again, it overwrites the current allowance with the amount.
* @dev `approve()` should only be callable by the bank or the owner of the account.
* @param `_operator` is the address to add to the set of authorized operators.
* @param `classId` is the class id of the bond.
* @param `_approved` is true if the operator is approved (based on the conditions provided), false meaning approval is revoked.
* @dev contract MUST define internal function regarding the conditions for setting approval and should be callable only by bank or owner.
* e.g: setApprovalFor(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,0,true);
* means that address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B is authorized to transfer bonds from class 0 (across all nonces).
*/

function setApprovalFor(address _operator, bool _approved) external returns(bool approved);

/**
* totalSupply
* @dev Here, total supply includes burned and redeemed supply.
* @param classId is the corresponding class Id of the bond.
* @param nonceId is the nonce Id of the given bond class.
* @return the supply of the bonds
* e.g:
* totalSupply(0, 1);
* it finds the total supply of the bonds of classid 0 and bond nonce 1.
*/
function totalSupply(uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* redeemedSupply
* @dev Returns the redeemed supply of the bond identified by (classId,nonceId).
* @param classId is the corresponding class id of the bond.
* @param nonceId is the nonce id of the given bond class.
* @return the supply of bonds redeemed.
*/
function redeemedSupply(uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* activeSupply
* @dev Returns the active supply of the bond defined by (classId,NonceId).
* @param classId is the corresponding classId of the bond.
* @param nonceId is the nonce id of the given bond class.
* @return the non-redeemed, active supply. 
*/
function activeSupply(uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* burnedSupply
* @dev Returns the burned supply of the bond in defined by (classId,NonceId).
* @param classId is the corresponding classId of the bond.
* @param nonceId is the nonce id of the given bond class.
* @return gets the supply of bonds for given classId and nonceId that are already burned.
*/
function burnedSupply(uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* balanceOf
* @dev Returns the balance of the bonds (nonReferenced) of given classId and bond nonce held by the address `_account`.
* @param classId is the corresponding classId of the bond.
* @param nonceId is the nonce id of the given bond class.
* @param _account address of the owner whose balance is to be determined.
* @dev this also consists of bonds that are redeemed.
*/
function balanceOf(address _account, uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* classMetadata
* @dev Returns the JSON metadata of the classes.
* @dev The metadata SHOULD follow a set of structures explained later in the metadata.md
* @param metadataId is the index-id given bond class information.
* @return the JSON metadata of the nonces. — e.g. `[title, type, description]`.
*/
function classMetadata(uint256 metadataId) external view returns (Metadata memory);

/**
* nonceMetadata 
* @dev Returns the JSON metadata of the nonces.
* @dev The metadata SHOULD follow a set of structures explained later in metadata.md
* @param classId is the corresponding classId of the bond.
* @param nonceId is the nonce id of the given bond class.
* @param metadataId is the index of the JSON storage for given metadata information. more is defined in metadata.md.
* @returns the JSON metadata of the nonces. — e.g. `[title, type, description]`.
*/
function nonceMetadata(uint256 classId, uint256 metadataId) external view returns (Metadata memory);

/**
* classValues
* @dev allows anyone to read the values (stored in struct Values for different class) for given bond class `classId`.
* @dev the values SHOULD follow a set of structures as explained in metadata along with correct mapping corresponding to the given metadata structure
* @param classId is the corresponding classId of the bond.
* @param metadataId is the index of the JSON storage for given metadata information of all values of given metadata. more is defined in metadata.md.
* @returns the Values of the class metadata. — e.g. `[string, uint, address]`.
*/
function classValues(uint256 classId, uint256 metadataId) external view returns (Values memory);

/**
* nonceValues
* @dev allows anyone to read the values (stored in struct Values for different class) for given bond (`nonceId`,`classId`).
* @dev the values SHOULD follow a set of structures explained in metadata along with correct mapping corresponding to the given metadata structure
* @param classId is the corresponding classId of the bond.
* @param metadataId is the index of the JSON storage for given metadata information of all values of given metadata. More is defined in metadata.md.
* @returns the Values of the class metadata. — e.g. `[string, uint, address]`.
*/
function nonceValues(uint256 classId, uint256 nonceId, uint256 metadataId) external view returns (Values memory);

/**
* getProgress
* @dev Returns the parameters to determine the current status of bonds maturity.
* @dev the conditions of redemption SHOULD be defined with one or several internal functions. 
* @param classId is the corresponding classId of the bond.
* @param nonceId is the nonceId of the given bond class . 
* @returns progressAchieved defines the metric (either related to % liquidity, time, etc.) that defines the current status of the bond.
* @returns progressRemaining defines the metric that defines the remaining time/ remaining progress. 
*/
function getProgress(uint256 classId, uint256 nonceId) external view returns (uint256 progressAchieved, uint256 progressRemaining);

/** 
* allowance
* @dev Authorizes to set the allowance for given `_spender` by `_owner` for all bonds identified by (classId, nonceId).
* @param _owner address of the owner of bond(and also msg.sender).
* @param _spender is the address authorized to spend the bonds held by _owner of info (classId, nonceId).
* @param classId is the corresponding classId of the bond.
* @param nonceId is the nonceId of the given bond class. 
* @notice Returns the _amount which spender is still allowed to withdraw from _owner.
*/
function allowance(address _owner, address _spender, uint256 classId, uint256 nonceId) external returns(uint256);

/** 
* isApprovedFor
* @dev returns true if address _operator is approved for managing the account’s bonds class.
* @notice Queries the approval status of an operator for a given owner.
* @dev _owner is the owner of bonds. 
* @dev _operator is the EOA /contract, whose status for approval on bond class for this approval is checked.
* @returns “true” if the operator is approved, “false” if not.
*/
function isApprovedFor(address _owner, address _operator) external view returns (bool);

Events

/** 
* Issue
* @notice Issue MUST trigger when Bonds are issued. This SHOULD not include zero value Issuing.
* @dev This SHOULD not include zero value issuing.
* @dev Issue MUST be triggered when the operator (i.e Bank address) contract issues bonds to the given entity.
* eg: emit Issue(_operator, 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]); 
* issue by address(operator) 500 Bonds(nonce14,class 1) to address 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef.
*/

event Issue(address indexed _operator, address indexed _to, Transaction[] _transactions); 

/** 
* Redeem
* @notice Redeem MUST trigger when Bonds are redeemed. This SHOULD not include zero value redemption.
*e.g: emit Redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[IERC3475.Transaction(1,14,500)]);
* emit event when 5000 bonds of class 1, nonce 14 owned by address 0x492Af743654549b12b1B807a9E0e8F397E44236E are being redeemed by 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef.
*/

event Redeem(address indexed _operator, address indexed _from, Transaction[] _transactions);


/** 
* Burn.
* @dev `Burn` MUST trigger when the bonds are being redeemed via staking (or being invalidated) by the bank contract.
* @dev `Burn` MUST trigger when Bonds are burned. This SHOULD not include zero value burning.
* e.g : emit Burn(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[IERC3475.Transaction(1,14,500)]);
* emits event when 500 bonds of owner 0x492Af743654549b12b1B807a9E0e8F397E44236E of type (class 1, nonce 14) are burned by operator  0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef.
*/

event burn(address _operator, address _owner, Transaction[] _transactions);

/** 
* Transfer
* @dev its emitted when the bond is transferred by address(operator) from owner address(_from) to address(_to) with the bonds transferred, whose params are defined by _transactions struct array. 
* @dev Transfer MUST trigger when Bonds are transferred. This SHOULD not include zero value transfers.
* @dev Transfer event with the _from `0x0` MUST not create this event(use `event Issued` instead). 
* e.g  emit Transfer(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E, _to, [IERC3475.Transaction(1,14,500)]);
* transfer by address(_operator) amount 500 bonds with (Class 1 and Nonce 14) from 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, to address(_to).
*/

event Transfer(address indexed _operator, address indexed _from, address indexed _to, Transaction[] _transactions);

/**
* ApprovalFor
* @dev its emitted when address(_owner) approves the address(_operator) to transfer his bonds.
* @notice Approval MUST trigger when bond holders are approving an _operator. This SHOULD not include zero value approval. 
* eg: emit ApprovalFor(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E, true);
* this means 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef gives 0x492Af743654549b12b1B807a9E0e8F397E44236E access permission for transfer of its bonds.
*/

event ApprovalFor(address indexed _owner, address indexed _operator, bool _approved);

Metadata: The metadata of a bond class or nonce is stored as an array of JSON objects, represented by the following types.

NOTE: all of the metadata schemas are referenced from here

1. Description:

This defines the additional information about the nature of data being stored in the nonce/class metadata structures. They are defined using the structured explained here. this will then be used by the frontend of the respective entities participating in the bond markets to interpret the data which is compliant with their jurisdiction.

2. Nonce:

The key value for indexing the information is the ‘class’ field. Following are the rules:

  • The title can be any alphanumeric type that is differentiated by the description of metadata (although it can be dependent on certain jurisdictions).
  • The title SHOULD not be EMPTY.

Some specific examples of metadata can be the localization of bonds, jurisdiction details etc., and they can be found in the metadata.md example description.

3. Class metadata:

This structure defines the details of the class information (symbol, risk information, etc.). the example is explained here in the class metadata section.

4. Decoding data

First, the functions for analyzing the metadata (i.e ClassMetadata and NonceMetadata) are to be used by the corresponding frontend to decode the information of the bond.

This is done via overriding the function interface for functions classValues and nonceValues by defining the key (which SHOULD be an index) to read the corresponding information stored as a JSON object.

{
"title": "symbol",
"_type": "string",
"description": "defines the unique identifier name in following format: (symbol, bondType, maturity in months)",
"values": ["Class Name 1","Class Name 2","DBIT Fix 6M"],
}

e.g. In the above example, to get the symbol of the given class id, we can use the class id as a key to get the symbol value in the values, which then can be used for fetching the detail for instance.

Rationale

Metadata structure

Instead of storing the details about the class and their issuances to the user (ie nonce) externally, we store the details in the respective structures. Classes represent the different bond types, and nonces represent the various period of issuances. Nonces under the same class share the same metadata. Meanwhile, nonces are non-fungible. Each nonce can store a different set of metadata. Thus, upon transfer of a bond, all the metadata will be transferred to the new owner of the bond.

 struct Values{
 string stringValue;
 uint uintValue;
 address addressValue;
 bool boolValue;
 bytes bytesValue;
 }
 struct Metadata {
 string title;
 string _type;
 string description;
 }

Batch function

This EIP supports batch operations. It allows the user to transfer different bonds along with their metadata to a new address instantaneously in a single transaction. After execution, the new owner holds the right to reclaim the face value of each of the bonds. This mechanism helps with the “packaging” of bonds–helpful in use cases like trades on a secondary market.

 struct Transaction {
 uint256 classId;
 uint256 nonceId;
 uint256 _amount;
 }

Where: The classId is the class id of the bond.

The nonceId is the nonce id of the given bond class. This param is for distinctions of the issuing conditions of the bond.

The _amount is the amount of the bond for which the spender is approved.

AMM optimization

One of the most obvious use cases of this EIP is the multilayered pool. The early version of AMM uses a separate smart contract and an EIP-20 LP token to manage a pair. By doing so, the overall liquidity inside of one pool is significantly reduced and thus generates unnecessary gas spent and slippage. Using this EIP standard, one can build a big liquidity pool with all the pairs inside (thanks to the presence of the data structures consisting of the liquidity corresponding to the given class and nonce of bonds). Thus by knowing the class and nonce of the bonds, the liquidity can be represented as the percentage of a given token pair for the owner of the bond in the given pool. Effectively, the EIP-20 LP token (defined by a unique smart contract in the pool factory contract) is aggregated into a single bond and consolidated into a single pool.

  • The reason behind the standard’s name (abstract storage bond) is its ability to store all the specifications (metadata/values and transaction as defined in the following sections) without needing external storage on-chain/off-chain.

Backwards Compatibility

Any contract that inherits the interface of this EIP is compatible. This compatibility exists for issuer and receiver of the bonds. Also any client EOA wallet can be compatible with the standard if they are able to sign issue() and redeem() commands.

However, any existing EIP-20 token contract can issue its bonds by delegating the minting role to a bank contract with the interface of this standard built-in. Check out our reference implementation for the correct interface definition.

To ensure the indexing of transactions throughout the bond lifecycle (i.e “Issue”, “Redeem” and “Transfer” functions), events cited in specification section MUST be emitted when such transaction is passed.

Note that the this standard interface is also compatible with EIP-20 and EIP-721 and EIP-1155interface.

However, creating a separate bank contract is recommended for reading the bonds and future upgrade needs.

Acceptable collateral can be in the form of fungible (like EIP-20), non-fungible (EIP-721, EIP-1155) , or other bonds represented by this standard.

Test Cases

Test-case for the minimal reference implementation is here. Use the Truffle box to compile and test the contracts.

Reference Implementation

Security Considerations

  • The function setApprovalFor(address _operatorAddress) gives the operator role to _operatorAddress. It has all the permissions to transfer, burn and redeem bonds by default.

  • If the owner wants to give a one-time allocation to an address for specific bonds(classId,bondsId), he should call the function approve() giving the Transaction[] allocated rather than approving all the classes using setApprovalFor.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Yu Liu (@yuliu-debond), Varun Deshpande (@dr-chain), Cedric Ngakam (@drikssy), Dhruv Malik (@dhruvmalik007), Samuel Gwlanold Edoumou (@Edoumou), Toufic Batrice (@toufic0710), "ERC-3475: Abstract Storage Bonds," Ethereum Improvement Proposals, no. 3475, April 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3475.