Alert Source Discuss
📢 Last Call Standards Track: ERC

ERC-7092: Financial Bonds

Represents debt issued by entities to investors.

Authors Samuel Gwlanold Edoumou (@Edoumou)
Created 2023-05-28
Last Call Deadline 2024-02-07
Requires EIP-165

Abstract

This proposal introduces fixed-income financial bonds with key characteristics defined to facilitate bond issuance in the primary market and enable buying or selling bonds in the secondary market. The standard also provides cross-chain functionalities for bonds operations and management accross multiple blockchains.

Motivation

Fixed-income instruments are a widely utilized asset class for corporations and other entities raising funds. However, transitioning to tokenized bonds is challenging due to existing standards like ERC-3475, which introduces unfamiliar concepts and leads to unnecessary gas consumption. Additionally, the lack of named variables like coupon, maturity date, and principal, makes it difficult to implement ERC-3475 since developers need to remember which metadata is assigned to each parameter.

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.

Every contract compliant with this ERC MUST implement the following Token Interface as well as the ERC-165 interface:

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

/**
* @title ERC-7092 Financial Bonds Standard
*/
interface IERC7092 /** is ERC165 */ {
    // events
    /**
    * @notice MUST be emitted when bond tokens are transferred, issued or redeemed, except during contract creation
    * @param _from the account that owns bonds
    * @param _to the account that receives the bond
    * @param _amount amount of bond tokens to be transferred
    */
    event Transfer(address indexed _from, address indexed _to, uint256 _amount);

    /**
    * @notice MUST be emitted when an account is approved or when the allowance is decreased
    * @param _owner bond token's owner
    * @param _spender the account to be allowed to spend bonds
    * @param _amount amount of bond tokens allowed by _owner to be spent by `_spender`
    *        Or amount of bond tokens to decrease allowance from `_spender`
    */
    event Approval(address indexed _owner, address indexed _spender, uint256 _amount);

    /**
    * @notice MUST be emitted when multiple bond tokens are transferred, issued or redeemed, with the exception being during contract creation
    * @param _from array of bondholders accounts
    * @param _to array of accounts to transfer bonds to
    * @param _amount array of amounts of bond tokens to be transferred
    *
    ** OPTIONAL - interfaces and other contracts MUST NOT expect this function to be present. MUST be emitted in `batchTransfer` and `batchTransferFrom` functions
    */
    event TransferBatch(address[] _from, address[] _to, uint256[] _amount);

    /**
    * @notice MUST be emitted when multiple accounts are approved or when the allowance is decreased from multiple accounts
    * @param _owner bondholder account
    * @param _spender array of accounts to be allowed to spend bonds, or to decrase the allowance from
    * @param _amount array of amounts of bond tokens allowed by `_owner` to be spent by multiple accounts in `_spender`.
    *
    ** OPTIONAL - interfaces and other contracts MUST NOT expect this function to be present. MUST be emitted in `batchApprove` and `batchDecreaseAllowance` functions
    */
    event ApprovalBatch(address indexed _owner, address[] _spender, uint256[] _amount);

    // getter functions
    /**
    *  @notice Returns the bond isin
    */
    function isin() external view returns(string memory);

    /**
    * @notice Returns the bond name
    */
    function name() external view returns(string memory);

    /**
    * @notice Returns the bond symbol
    *         It is RECOMMENDED to represent the symbol as a combination of the issuer Issuer'shorter name and the maturity date
    *         Ex: If a company named Green Energy issues bonds that will mature on october 25, 2030, the bond symbol could be `GE30` or `GE2030` or `GE102530`
    */
    function symbol() external view returns(string memory);

    /**
    * @notice Returns the bond currency. This is the contract address of the token used to pay and return the bond principal
    */
    function currency() external view returns(address);

    /**
    * @notice Returns the bond denominiation. This is the minimum amount in which the Bonds may be issued. It must be expressend in unit of the principal currency
    *         ex: If the denomination is equal to 1,000 and the currency is USDC, then the bond denomination is equal to 1,000 USDC
    */
    function denomination() external view returns(uint256);

    /**
    * @notice Returns the issue volume (total debt amount). It is RECOMMENDED to express the issue volume in denomination unit.
    */
    function issueVolume() external view returns(uint256);

    /**
    * @notice Returns the bond interest rate. It is RECOMMENDED to express the interest rate in basis point unit.
    *         1 basis point = 0.01% = 0.0001
    *         ex: if interest rate = 5%, then coupon() => 500 basis points
    */
    function couponRate() external view returns(uint256);

    /**
    * @notice Returns the date when bonds were issued to investors. This is a Unix Timestamp like the one returned by block.timestamp
    */
    function issueDate() external view returns(uint256);

    /**
    * @notice Returns the bond maturity date, i.e, the date when the pricipal is repaid. This is a Unix Timestamp like the one returned by block.timestamp
    *         The maturity date MUST be greater than the issue date
    */
    function maturityDate() external view returns(uint256);

    /**
    * @notice Returns the principal of an account. It is RECOMMENDED to express the principal in the bond currency unit (USDC, DAI, etc...)
    * @param _account account address
    */
    function principalOf(address _account) external view returns(uint256);

    /**
    * @notice Returns the amount of tokens the `_spender` account has been authorized by the `_owner``
    *         acount to manage their bonds
    * @param _owner the bondholder address
    * @param _spender the address that has been authorized by the bondholder
    */
    function allowance(address _owner, address _spender) external view returns(uint256);

    // setter functions
    /**
    * @notice Authorizes `_spender` account to manage `_amount`of their bond tokens
    * @param _spender the address to be authorized by the bondholder
    * @param _amount amount of bond tokens to approve
    */
    function approve(address _spender, uint256 _amount) external returns(bool);

    /**
    * @notice Lowers the allowance of `_spender` by `_amount`
    * @param _spender the address to be authorized by the bondholder
    * @param _amount amount of bond tokens to remove from allowance
    */
    function decreaseAllowance(address _spender, uint256 _amount) external returns(bool);

    /**
    * @notice Moves `_amount` bonds to address `_to`. This methods also allows to attach data to the token that is being transferred
    * @param _to the address to send the bonds to
    * @param _amount amount of bond tokens to transfer
    * @param _data additional information provided by the token holder
    */
    function transfer(address _to, uint256 _amount, bytes calldata _data) external returns(bool);

    /**
    * @notice Moves `_amount` bonds from an account that has authorized the caller through the approve function
    *         This methods also allows to attach data to the token that is being transferred
    * @param _from the bondholder address
    * @param _to the address to transfer bonds to
    * @param _amount amount of bond tokens to transfer.
    * @param _data additional information provided by the token holder
    */
    function transferFrom(address _from, address _to, uint256 _amount, bytes calldata _data) external returns(bool);

    // batch functions
    /**
    * @notice Authorizes multiple spender accounts to manage a specified `_amount` of the bondholder tokens
    * @param _spender array of accounts to be authorized by the bondholder
    * @param _amount array of amounts of bond tokens to approve
    *
    * OPTIONAL - interfaces and other contracts MUST NOT expect these values to be present. The method is used to improve usability.
    */
    function batchApprove(address[] calldata _spender, uint256[] calldata _amount) external returns(bool);

    /**
    * @notice Decreases the allowance of multiple spenders by corresponding amounts in `_amount`
    * @param _spender array of accounts to be authorized by the bondholder
    * @param _amount array of amounts of bond tokens to decrease the allowance from
    *
    * OPTIONAL - interfaces and other contracts MUST NOT expect this function to be present. The method is used to decrease token allowance.
    */
    function batchDecreaseAllowance(address[] calldata _spender, uint256[] calldata _amount) external returns(bool);

    /**
    * @notice Transfers multiple bonds with amounts specified in the array `_amount` to the corresponding accounts in the array `_to`, with the option to attach additional data
    * @param _to array of accounts to send the bonds to
    * @param _amount array of amounts of bond tokens to transfer
    * @param _data array of additional information provided by the token holder
    *
    * OPTIONAL - interfaces and other contracts MUST NOT expect this function to be present.
    */
    function batchTransfer(address[] calldata _to, uint256[] calldata _amount, bytes[] calldata _data) external returns(bool);

    /**
    * @notice Transfers multiple bonds with amounts specified in the array `_amount` to the corresponding accounts in the array `_to` from an account that have been authorized by the `_from` account
    *         This method also allows to attach data to tokens that are being transferred
    * @param _from array of bondholder accounts
    * @param _to array of accounts to transfer bond tokens to
    * @param _amount array of amounts of bond tokens to transfer.
    * @param _data array of additional information provided by the token holder
    *
    ** OPTIONAL - interfaces and other contracts MUST NOT expect this function to be present.
    */
    function batchTransferFrom(address[] calldata _from, address[] calldata _to, uint256[] calldata _amount, bytes[] calldata _data) external returns(bool);
}

Additional bond parameters Interface

The IERC7092ESG interface is OPTIONAL for contracts implementing this proposal. This interface MAY be used to improve the standard usability.

  • The currencyOfCoupon The currency used for coupon payment may be different from the currency used to repay the principal
  • The couponType MAY be employed to signify the interest rate that the issuer has committed to paying to investors, which may take various forms such as zero coupon, fixed rate, floating rate, and more.
  • The couponFrequency refers to how often the bond pays interest to its bondholders, and is typically expressed in terms of time periods, such as: Annual, Semi-Annual, Quarterly, or Monthly.
  • The dayCountBasis is used to calculate the accrued interest on a bond between two coupon payment dates or other specific periods. Some of the day count basis are: Actual/Actual, 30/360, Actual/360, Actual/365, or 30/365
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

interface IERC7092ESG /** is ERC165 */ {
    /**
    * @notice Returns the number of decimals used by the bond. For example, if it returns `10`, it means that the token amount MUST be multiplied by 10000000000 to get the standard representation.
    */
    function decimals() external view returns(uint8);

    /**
    * @notice Rreturns the coupon currency, which is represented by the contract address of the token used to pay coupons. It can be the same as the one used for the principal
    */
    function currencyOfCoupon() external view returns(address);

    /**
    * @notice Returns the coupon type
    *         For example, 0 can denote Zero coupon, 1 can denote Fixed Rate, 2 can denote Floating Rate, and so on
    */
    function couponType() external view returns(uint8);

    /**
    * @notice Returns the coupon frequency, i.e. the number of times coupons are paid in a year.
    */
    function couponFrequency() external view returns(uint256);

    /**
    * @notice Returns the day count basis
    *         For example, 0 can denote actual/actual, 1 can denote actual/360, and so on
    */
    function dayCountBasis() external view returns(uint8);
}

Cross-chain Interface

The standard permits the implementation of the IERC7092CrossChain interface for cross-chain management of bond tokens. This interface is OPTIONAL and may be used by applications to allow cross-chain transactions. Any function initiating a cross-chain transaction MUST explicitly define the destination chain identifier destinationChainID and specify the target smart contract destinationContract.

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

interface IERC7092CrossChain /** is ERC165 */ {
    // events
    /**
    * @notice MUST be emitted when bond tokens are transferred or redeemed in a cross-chain transaction
    * @param _from bondholder account
    * @param _to account the transfer bond tokens to
    * @param _amount amount of bond tokens to be transferred
    * @param _destinationChainID The unique ID that identifies the destination Chain
    */
    event CrossChainTransfer(address indexed _from, address indexed _to, uint256 _amount, bytes32 _destinationChainID);

    /**
    * @notice MUST be emitted when several bond tokens are transferred or redeemed in a cross-chain transaction
    * @param _from array of bondholders accounts
    * @param _to array of accounts that receive the bond
    * @param _amount array of amount of bond tokens to be transferred
    * @param _destinationChainID array of unique IDs that identify the destination Chain
    */
    event CrossChainTransferBatch(address[] _from, address[] _to, uint256[] _amount, bytes32[] _destinationChainID);

    /**
    * @notice MUST be emitted when an account is approved to spend the bondholder's tokens in a different chain than the current chain
    * @param _owner the bondholder account
    * @param _spender the account to be allowed to spend bonds
    * @param _amount amount of bond tokens allowed by `_owner` to be spent by `_spender`
    * @param _destinationChainID The unique ID that identifies the destination Chain
    */
    event CrossChainApproval(address indexed _owner, address indexed _spender, uint256 _amount, bytes32 _destinationChainID);

    /**
    * @notice MUST be emitted when multiple accounts in the array `_spender` are approved or when the allowances of multiple accounts in the array `_spender` are reduced on the destination chain which MUST be different than the current chain
    * @param _owner bond token's owner
    * @param _spender array of accounts to be allowed to spend bonds
    * @param _amount array of amount of bond tokens allowed by _owner to be spent by _spender
    * @param _destinationChainID array of unique IDs that identify the destination Chain
    */
    event CrossChainApprovalBatch(address indexed _owner, address[] _spender, uint256[] _amount, bytes32[] _destinationChainID);

    // functions
    /**
    * @notice Authorizes the `_spender` account to manage a specified `_amount`of the bondholder bond tokens on the destination Chain
    * @param _spender account to be authorized by the bondholder
    * @param _amount amount of bond tokens to approve
    * @param _destinationChainID The unique ID that identifies the destination Chain.
    * @param _destinationContract The smart contract to interact with in the destination Chain
    */
    function crossChainApprove(address _spender, uint256 _amount, bytes32 _destinationChainID, address _destinationContract) external returns(bool);

    /**
    * @notice Authorizes multiple spender accounts in `_spender` to manage specified amounts in `_amount` of the bondholder tokens on the destination chain
    * @param _spender array of accounts to be authorized by the bondholder
    * @param _amount array of amounts of bond tokens to approve
    * @param _destinationChainID array of unique IDs that identifies the destination Chain.
    * @param _destinationContract array of smart contracts to interact with in the destination Chain in order to Deposit or Mint tokens that are transferred.
    */
    function crossChainBatchApprove(address[] calldata _spender, uint256[] calldata _amount, bytes32[] calldata _destinationChainID, address[] calldata _destinationContract) external returns(bool);

    /**
    * @notice Decreases the allowance of `_spender` by a specified `_amount` on the destination Chain
    * @param _spender the address to be authorized by the bondholder
    * @param _amount amount of bond tokens to remove from allowance
    * @param _destinationChainID The unique ID that identifies the destination Chain.
    * @param _destinationContract The smart contract to interact with in the destination Chain in order to Deposit or Mint tokens that are transferred.
    */
    function crossChainDecreaseAllowance(address _spender, uint256 _amount, bytes32 _destinationChainID, address _destinationContract) external returns(bool);

    /**
    * @notice Decreases the allowance of multiple spenders in `_spender` by corresponding amounts specified in the array `_amount` on the destination chain
    * @param _spender array of accounts to be authorized by the bondholder
    * @param _amount array of amounts of bond tokens to decrease the allowance from
    * @param _destinationChainID array of unique IDs that identifies the destination Chain.
    * @param _destinationContract array of smart contracts to interact with in the destination Chain in order to Deposit or Mint tokens that are transferred.
    */
    function crossChainBatchDecreaseAllowance(address[] calldata _spender, uint256[] calldata _amount, bytes32[] calldata _destinationChainID, address[] calldata _destinationContract) external returns(bool);

    /**
    * @notice Moves `_amount` bond tokens to the address `_to` from the current chain to another chain (e.g., moving tokens from Ethereum to Polygon).
    *         This methods also allows to attach data to the token that is being transferred
    * @param _to account to send bond tokens to
    * @param _amount amount of bond tokens to transfer
    * @param _data additional information provided by the bondholder
    * @param _destinationChainID The unique ID that identifies the destination Chain.
    * @param _destinationContract The smart contract to interact with in the destination Chain in order to Deposit or Mint bond tokens that are transferred.
    */
    function crossChainTransfer(address _to, uint256 _amount, bytes calldata _data, bytes32 _destinationChainID, address _destinationContract) external returns(bool);

    /**
    * @notice Transfers multiple bond tokens with amounts specified in the array `_amount` to the corresponding accounts in the array `_to` from the current chain to another chain (e.g., moving tokens from Ethereum to Polygon).
    *         This methods also allows to attach data to the token that is being transferred
    * @param _to array of accounts to send the bonds to
    * @param _amount array of amounts of bond tokens to transfer
    * @param _data array of additional information provided by the bondholder
    * @param _destinationChainID array of unique IDs that identify the destination Chains.
    * @param _destinationContract array of smart contracts to interact with in the destination Chains in order to Deposit or Mint bond tokens that are transferred.
    */
    function crossChainBatchTransfer(address[] calldata _to, uint256[] calldata _amount, bytes[] calldata _data, bytes32[] calldata _destinationChainID, address[] calldata _destinationContract) external returns(bool);

    /**
    * @notice Transfers `_amount` bond tokens from the `_from`account to the `_to` account from the current chain to another chain. The caller must be approved by the `_from` address.
    *         This methods also allows to attach data to the token that is being transferred
    * @param _from the bondholder address
    * @param _to the account to transfer bonds to
    * @param _amount amount of bond tokens to transfer
    * @param _data additional information provided by the token holder
    * @param _destinationChainID The unique ID that identifies the destination Chain.
    * @param _destinationContract The smart contract to interact with in the destination Chain in order to Deposit or Mint tokens that are transferred.
    */
    function crossChainTransferFrom(address _from, address _to, uint256 _amount, bytes calldata _data, bytes32 _destinationChainID, address _destinationContract) external returns(bool);

    /**
    * @notice Transfers several bond tokens with amounts specified in the array `_amount` from accounts in the array `_from` to accounts in the array `_to` from the current chain to another chain.
    *         The caller must be approved by the `_from` accounts to spend the corresponding amounts specified in the array `_amount`
    *         This methods also allows to attach data to the token that is being transferred
    * @param _from array of bondholder addresses
    * @param _to array of accounts to transfer bonds to
    * @param _amount array of amounts of bond tokens to transfer
    * @param _data array of additional information provided by the token holder
    * @param _destinationChainID array of unique IDs that identifies the destination Chain.
    * @param _destinationContract array of smart contracts to interact with in the destination Chain in order to Deposit or Mint tokens that are transferred.
    */
    function crossChainBatchTransferFrom(address[] calldata _from, address[] calldata _to, uint256[] calldata _amount, bytes[] calldata _data, bytes32[] calldata _destinationChainID, address[] calldata _destinationContract) external returns(bool);
}

Rationale

The design of this ERC aims to simplify the migration to tokenized bonds by maintaining consistency with traditional bond standards. This approach allows fixed-income instruments to be represented as on-chain tokens, manageable through wallets, and utilized by applications like decentralized exchanges, while avoiding the complexities and inefficiencies associated with other standards. This ERC facilitates the creation of new bond tokens with characteristics akin to traditional bonds, enhancing accessibility, liquidity, and cost-efficiency in bond trading and management.

The use of traditional finance terminology, like issueVolume and principalOf, is aimed at maintaining consistency with traditional bond language, which eases the adaptation for traditional entities.

Total Supply and Account Balance

The totalSupply and balanceOf functions are not defined as they can be derived from issueVolume and principalOf, and denomination. However, these functions can be be added in any contract implementing this standard, ensuring the proper relationship between these values.

    function totalSupply() external view returns(uint256) {
        return issueVolume() / denomination();
    }

    function balance0f(account) external view returns(uint256) {
        return principal(account) / denomination();
    }

Backwards Compatibility

This ERC is not backwards compatible with existing standards like ERC-20 or ERC-1155 due to the absence of certain functions like totalSupply or balanceOf. A pure implementation of this standard is RECOMMENDED for issuing tokenized bonds, as any hybrid solution with other mentioned standards SHOULD fail.

Reference Implementation

The complete Reference Implementation can be found here.

Bonds with embedded options like callable, puttable, or convertible bonds can be created by inheriting from the reference ERC7092.sol that integrates the proposed interface.

CALLABLE BONDS:

pragma solidity ^0.8.0;

import 'ERC7092.sol';

contract ERC7092Callable is ERC7092 {
    // WRITE THE LOGIC TO ALLOW THE ISSUER TO CALL BONDS
    // STATE VARIABLES AND FUNCTIONS NEEDED
    
    /**
    * @notice call bonds owned by `_investor`
    *         MUST be called by the issuer only
    */
    function call(address _investor) public {
        require(msg.sender == _issuer[bondISIN].issuerAddress, "ERC7092Callable: ONLY_ISSUER");
        require(_principals[_investor] > 0, "ERC7092Callable: NO_BONDS");
        require(block.timestamp < _bond[bondISIN].maturityDate, "ERC7092Callable: BOND_MATURED");
        
        uint256 principal =  _principals[_investor];
        _principals[_investor] = 0;
        
        // ADD LOGIC HERE
    }
}

PUTTABLE BONDS:

pragma solidity ^0.8.0;

import 'ERC7092.sol';

contract ERC7092Puttable is ERC7092 {
    // WRITE THE LOGIC TO ALLOW INVESTORS TO PUT BONDS
    // STATE VARIABLES AND FUNCTIONS NEEDED
    
    /**
    * @notice put bonds
    *         MUST be called by investors who own bonds
    */
    function put() public {
        require(_principals[msg.sender] > 0, "ERC7092Puttable: ONLY_INVESTORS");
        require(block.timestamp < _bond[bondISIN].maturityDate, "ERC7092Puttable: BOND_MATURED");
        
        uint256 principal =  _principals[msg.sender];
        _principals[msg.sender] = 0;
        
        // ADD LOGIC
    }
}

CONVERTIBLE BONDS:

pragma solidity ^0.8.0;

import 'ERC7092.sol';

contract ERC7092Convertible is ERC7092 {
    // WRITE THE LOGIC TO ALLOW INVESTOR OR ISSUER TO CONVERT BONDS TO EQUITY
    // STATE VARIABLES AND FUNCTIONS NEEDED
    
    /**
    * @notice convert bonds to equity. Here we assumed that the investors must convert their bonds to equity
    *         Issuer can also convert invetsors bonds to equity.
    */
    function convert() public {
        require(_principals[msg.sender] > 0, "ERC7092Convertible: ONLY_INVESTORS");
        require(block.timestamp < _bond[bondISIN].maturityDate, "ERC7092Convertible: BOND_MATURED");
        
        uint256 principal =  _principals[msg.sender];
        _principals[msg.sender] = 0;
        
        // ADD LOGIC HERE
    }
}

Identity Registry

This standard is designed specifically for tokenizing bonds. It does not inherently manage information pertaining to bondholders’ identities. However, to enhance compliance with regulatory requirements and improve transparency, an identity registry can be added on top of this standard to store the identity of all authorized investors.

By maintaining an identity registry, issuers can ensure that bond tokens issued under the ERC7092 standard are transferred only to registered and authorized entities. This practice aligns with regulatory compliance measures and provides a structured way to manage and verify the identity of bondholders. It also helps prevent unauthorized or non-compliant transfers of bond tokens.

Security Considerations

Implementing this ERC requires careful consideration of security risks related to functions approving operators to manage owner’s bonds and functions allowing bond transfers. The use of these functions necessitates robust validation to ensure only the bond owner or approved accounts can call them.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Samuel Gwlanold Edoumou (@Edoumou), "ERC-7092: Financial Bonds [DRAFT]," Ethereum Improvement Proposals, no. 7092, May 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7092.