Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-5095: 本金代币

本金代币(零息代币)可在未来的时间戳兑换为单个底层 EIP-20 代币。

Authors Julian Traversa (@JTraversa), Robert Robbins (@robrobbins), Alberto Cuesta Cañada (@alcueca)
Created 2022-05-01
Discussion Link https://ethereum-magicians.org/t/eip-5095-principal-token-standard/9259
Requires EIP-20, EIP-2612

摘要

本金代币代表在未来时间戳时底层 EIP-20 代币的所有权。

本规范是 EIP-20 代币的扩展,提供了用于存入 和提取代币以及读取余额的基本功能,以及提供基于 EIP-712 签名的 EIP-2612 规范。

动机

本金代币缺乏标准化,这导致了难以导航的开发空间和多样化的实现 方案。

主要的例子包括剥离未来收益并留下本金 代币的收益代币化平台,以及利用本金代币作为媒介的固定利率货币市场来进行借贷。

这种实现上的不一致使得在应用层以及钱包层 的集成变得困难,而这些是该领域增长的关键催化剂。开发人员目前需要为每个本金代币实现单独的适配器,以及为其池合约的适配器, 并且很多时候还需要为其托管合约的适配器,从而浪费了大量的开发人员资源。

规范

所有本金代币(PT)必须实现 EIP-20 以表示未来底层赎回的所有权。 如果 PT 是不可转让的,则它可能在调用 transfertransferFrom 时回退。 EIP-20 操作 balanceOftransfertotalSupply 等作用于本金代币余额。

所有本金代币必须实现 EIP-20 的可选元数据扩展。 namesymbol 函数应以某种方式反映底层代币的 namesymbol,以及原始协议,并且在收益代币化协议的情况下,反映原始货币市场。

所有本金代币都可以实现 EIP-2612 以改善在各种集成上批准 PT 的 UX。

定义:

  • underlying(底层资产):在到期时可用于赎回本金代币的代币。 具有由相应的 EIP-20 合约定义的单位。
  • maturity(到期日):本金代币到期的 Unix 时间戳(uint256)。本金代币在此时间戳或之后可以赎回底层资产。
  • fee(费用):本金代币向用户收取的底层资产或本金代币的数量。费用可能存在于赎回或到期后收益中。
  • slippage(滑点):广告宣传的赎回价值与 PT 赎回的经济现实之间的任何差异,费用未考虑在内。

方法

underlying

本金代币用于记账和赎回的底层代币的地址。

必须是 EIP-20 代币合约。

不得回退。

- name: underlying
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: underlyingAddress
      type: address

maturity

可以赎回本金代币以获取其底层存款的 Unix 时间戳(uint256),在此时间戳或之后。

不得回退。

- name: maturity
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: timestamp
      type: uint256

convertToUnderlying

在满足所有条件的理想情况下,将为提供的 PT 数量兑换的底层资产数量。

在到期之前,返回的底层资产数量就好像 PT 将在到期时一样。

必须不包括对赎回收取的任何费用。

不得显示任何取决于调用者的变化。

执行实际赎回时,不得反映滑点或其他链上条件。

除非由于不合理的大输入导致整数溢出,否则不得回退。

必须向下舍入为 0。

此计算可能不反映“每个用户的”本金代币价格,而应反映“平均用户的”本金代币价格,这意味着平均用户在往返兑换时应期望看到的价格。

- name: convertToUnderlying
  type: function
  stateMutability: view

  inputs:
    - name: principalAmount
      type: uint256

  outputs:
    - name: underlyingAmount
      type: uint256

convertToPrincipal

为了提供指定的底层资产数量,本金代币合约将请求赎回的本金代币数量,在满足所有条件的理想情况下。

必须不包括任何费用。

不得显示任何取决于调用者的变化。

不得反映滑点或其他链上条件,在执行实际兑换时。

除非由于不合理的大输入导致整数溢出,否则不得回退。

必须向下舍入为 0。

此计算可能不反映“每个用户的”本金代币价格,而应反映“平均用户的”本金代币价格,这意味着平均用户在赎回时应期望看到的价格。

- name: convertToPrincipal
  type: function
  stateMutability: view

  inputs:
    - name: underlyingAmount
      type: uint256

  outputs:
    - name: principalAmount
      type: uint256

maxRedeem

可以通过 redeem 调用从 holder 余额中赎回的最大本金代币数量。

必须返回可以通过 redeemholder 转移的最大本金代币数量,并且不会导致回退,该数量不得高于将被接受的实际最大值(如果必要,应低估)。

必须考虑全局和用户特定的限制,例如如果完全禁用赎回(即使是暂时的),则必须返回 0。

不得回退。

- name: maxRedeem
  type: function
  stateMutability: view

  inputs:
    - name: holder
      type: address

  outputs:
    - name: maxPrincipalAmount
      type: uint256

previewRedeem

允许链上或链下用户在当前区块模拟其赎回的效果,给定当前的链上条件。

必须返回尽可能接近且不超过在同一交易中 redeem 调用中获得的准确的 underlying 数量。即,如果在同一交易中调用,redeem 应返回与 previewRedeem 相同或更多的 underlyingAmount

不得考虑赎回限制,例如从 maxRedeem 返回的那些限制,并且应始终表现得好像赎回将被接受,无论用户是否有足够的本金代币等。

必须包括赎回费用。集成商应意识到赎回费用的存在。

不得由于本金代币合约特定的用户/全局限制而回退。可能由于其他也会导致 redeem 回退的条件而回退。

请注意,convertToUnderlyingpreviewRedeem 之间的任何不利差异应被视为每个本金代币的价格或某种其他类型的条件中的滑点。

- name: previewRedeem
  type: function
  stateMutability: view

  inputs:
    - name: principalAmount
      type: uint256

  outputs:
    - name: underlyingAmount
      type: uint256

redeem

在到期时或之后,从 from 燃烧确切的 principalAmount 的本金代币,并将 underlyingAmount 的底层代币发送到 to

接口和其他合约不得期望存在资金托管。虽然通过本金代币合约进行本金代币的托管赎回对于集成商非常有用,但某些协议可能会发现将本金代币本身托管会破坏其向后兼容性。

必须发出 Redeem 事件。

必须支持一种赎回流程,其中本金代币直接从 holder 燃烧,其中 holdermsg.sendermsg.senderholder 的本金代币具有 EIP-20 批准。 可以支持另一种流程,其中本金代币在 redeem 执行之前转移到本金代币合约,并在 redeem 期间进行核算。

如果无法赎回所有 principalAmount(由于达到提款限制、滑点、持有者没有足够的本金代币等),则必须回退。

请注意,某些实现将需要在执行提款之前预先请求本金代币。这些方法应单独执行。

- name: redeem
  type: function
  stateMutability: nonpayable

  inputs:
    - name: principalAmount
      type: uint256
    - name: to
      type: address
    - name: from
      type: address

  outputs:
    - name: underlyingAmount
      type: uint256

maxWithdraw

可以通过 withdraw 调用从 holder 本金代币余额中赎回的最大底层资产数量。

必须返回可以通过 withdrawholder 赎回的最大底层代币数量,并且不会导致回退,该数量不得高于将被接受的实际最大值(如果必要,应低估)。

必须考虑全局和用户特定的限制,例如如果完全禁用提款(即使是暂时的),则必须返回 0。

不得回退。

- name: maxWithdraw
  type: function
  stateMutability: view

  inputs:
    - name: holder
      type: address

  outputs:
    - name: maxUnderlyingAmount
      type: uint256

previewWithdraw

允许链上或链下用户在当前区块模拟其提款的效果,给定当前的链上条件。

必须返回尽可能接近且不少于在同一交易中 withdraw 调用中将被燃烧的准确的本金代币数量。即,如果在同一交易中调用,withdraw 应返回与 previewWithdraw 相同或更少的 principalAmount

不得考虑提款限制,例如从 maxWithdraw 返回的那些限制,并且应始终表现得好像提款将被接受,无论用户是否有足够的本金代币等。

必须包括提款费用。集成商应意识到提款费用的存在。

不得由于本金代币合约特定的用户/全局限制而回退。可能由于其他也会导致 withdraw 回退的条件而回退。

请注意,convertToPrincipalpreviewWithdraw 之间的任何不利差异应被视为每个本金代币的价格或某种其他类型的条件中的滑点。

- name: previewWithdraw
  type: function
  stateMutability: view

  inputs:
    - name: underlyingAmount
      type: uint256

  outputs:
    - name: principalAmount
      type: uint256

withdraw

holder 燃烧 principalAmount,并将确切的 underlyingAmount 的底层代币发送到 receiver

必须发出 Redeem 事件。

必须支持一种提款流程,其中本金代币直接从 holder 燃烧,其中 holdermsg.sendermsg.senderholder 的本金代币具有 EIP-20 批准。 可以支持另一种流程,其中本金代币在 withdraw 执行之前转移到本金代币合约,并在 withdraw 期间进行核算。

如果无法提取所有 underlyingAmount(由于达到提款限制、滑点、持有者没有足够的本金代币等),则必须回退。

请注意,某些实现将需要在执行提款之前预先请求本金代币合约。这些方法应单独执行。

- name: withdraw
  type: function
  stateMutability: nonpayable

  inputs:
    - name: underlyingAmount
      type: uint256
    - name: receiver
      type: address
    - name: holder
      type: address

  outputs:
    - name: principalAmount
      type: uint256

事件

Redeem

from 已将 principalAmount 的本金代币兑换为 underlyingAmount 的底层资产,并将该底层资产转移到 to

当在本金代币合约中的 EIP5095.redeem 方法中燃烧本金代币并提取底层资产时,必须发出。

- name: Redeem
  type: event

  inputs:
    - name: from
      indexed: true
      type: address
    - name: to
      indexed: true
      type: address
    - name: amount
      indexed: false
      type: uint256

理由

本金代币接口旨在针对集成商进行优化,具有核心最小接口以及可选接口以实现向后兼容性。底层资产的会计和管理等详细信息有意未指定,因为预计本金代币在链上会被视为黑盒,并在使用前在链下进行检查。

强制执行 EIP-20,因为代币批准和余额计算等实现细节直接延续。这种标准化使得本金代币除了 EIP-5095 之外,还可以立即与所有 EIP-20 用例兼容。

所有本金代币都可以在到期时赎回,唯一的区别是到期后是否会产生进一步的收益。鉴于赎回的普遍性,redeem 的存在允许集成商在公开市场上购买本金代币,然后仅凭本金代币本身的地址将其赎回为固定收益。

本 EIP 在很大程度上借鉴了 EIP-4626 的设计,因为从技术上讲,本金代币可以被描述为收益承担金库的一个子集,扩展了 maturity 变量并限制了实现。 但是,扩展 EIP-4626 将迫使 PT 实现包括 PT 解决的业务案例不需要的方法(即 mintdeposit)。也可以认为部分赎回(通过 withdraw 实现)对于 PT 来说很少见。

PT 在精确的秒数到期,但鉴于智能合约的反应性质,不能有标记到期的事件,因为不能保证在到期时或之后有任何活动。在到期后的第一个交易中发出事件以通知到期将是不精确且昂贵的。相反,建议集成商使用第一个 Redeem 事件,或者自己跟踪预计每个 PT 何时到期。

向后兼容性

本 EIP 与 EIP-20 规范完全向后兼容,并且与其他标准没有已知的兼容性问题。 对于不使用 EIP-5095 的本金代币的生产实现,可以开发和使用包装器适配器,或者可以实现包装的代币。

参考实现

// SPDX-License-Identifier: MIT
pragma solidity 0.8.14;

import {ERC20} from "yield-utils-v2/contracts/token/ERC20.sol";
import {MinimalTransferHelper} from "yield-utils-v2/contracts/token/MinimalTransferHelper.sol";

contract ERC5095 is ERC20 {
    using MinimalTransferHelper for ERC20;

    /* EVENTS
     *****************************************************************************************************************/

    event Redeem(address indexed from, address indexed to, uint256 underlyingAmount);

    /* MODIFIERS
     *****************************************************************************************************************/

    /// @notice A modifier that ensures the current block timestamp is at or after maturity.
    // @notice 确保当前区块时间戳在到期日或之后的修饰符。
    modifier afterMaturity() virtual {
        require(block.timestamp >= maturity, "BEFORE_MATURITY");
        _;
    }

    /* IMMUTABLES
     *****************************************************************************************************************/

    ERC20 public immutable underlying;
    uint256 public immutable maturity;

    /* CONSTRUCTOR
     *****************************************************************************************************************/

    constructor(
        string memory name_,
        string memory symbol_,
        uint8 decimals_,
        ERC20 underlying_,
        uint256 maturity_
    ) ERC20(name_, symbol_, decimals_) {
        underlying = underlying_;
        maturity = maturity_;
    }

    /* CORE FUNCTIONS
     *****************************************************************************************************************/

    /// @notice Burns an exact amount of principal tokens in exchange for an amount of underlying.
    // @notice 燃烧确切数量的本金代币以换取一定数量的基础资产。
    /// @dev This reverts if before maturity.
    // @dev 如果在到期之前,则会回退。
    /// @param principalAmount The exact amount of principal tokens to be burned.
    // @param principalAmount 要燃烧的确切数量的本金代币。
    /// @param from The owner of the principal tokens to be redeemed.  If not msg.sender then must have prior approval.
    // @param from 要赎回的本金代币的所有者。如果不是 msg.sender,则必须事先获得批准。
    /// @param to The address to send the underlying tokens.
    // @param to 发送基础代币的地址。
    /// @return underlyingAmount The total amount of underlying tokens sent.
    // @return underlyingAmount 发送的基础代币的总量。
    function redeem(
        uint256 principalAmount,
        address from,
        address to
    ) public virtual afterMaturity returns (uint256 underlyingAmount) {
        _decreaseAllowance(from, principalAmount);

        // Check for rounding error since we round down in previewRedeem.
        // 检查舍入误差,因为我们在 previewRedeem 中向下舍入。
        require((underlyingAmount = _previewRedeem(principalAmount)) != 0, "ZERO_ASSETS");

        _burn(from, principalAmount);

        emit Redeem(from, to, principalAmount);

        _transferOut(to, underlyingAmount);
    }

    /// @notice Burns a calculated amount of principal tokens in exchange for an exact amount of underlying.
    // @notice 燃烧计算数量的本金代币以换取确切数量的基础资产。
    /// @dev This reverts if before maturity.
    // @dev 如果在到期之前,则会回退。
    /// @param underlyingAmount The exact amount of underlying tokens to be received.
    // @param underlyingAmount 要接收的基础代币的精确金额。
    /// @param from The owner of the principal tokens to be redeemed.  If not msg.sender then must have prior approval.
    // @param from 要赎回的本金代币的所有者。如果不是 msg.sender,则必须事先获得批准。
    /// @param to The address to send the underlying tokens.
    // @param to 发送基础代币的地址。
    /// @return principalAmount The total amount of underlying tokens redeemed.
    // @return principalAmount 赎回的基础代币的总量。
    function withdraw(
        uint256 underlyingAmount,
        address from,
        address to
    ) public virtual afterMaturity returns (uint256 principalAmount) {
        principalAmount = _previewWithdraw(underlyingAmount); // No need to check for rounding error, previewWithdraw rounds up.
        // 无需检查舍入误差,previewWithdraw 向上舍入。

        _decreaseAllowance(from, principalAmount);

        _burn(from, principalAmount);

        emit Redeem(from, to, principalAmount);

        _transferOut(to, underlyingAmount);
    }

    /// @notice An internal, overridable transfer function.
    // @notice 内部可覆盖的 transfer 函数。
    /// @dev Reverts on failed transfer.
    // @dev 传输失败时回退。
    /// @param to The recipient of the transfer.
    // @param to transfer 的接收者。
    /// @param amount The amount of the transfer.
    // @param amount transfer 的数额。
    function _transferOut(address to, uint256 amount) internal virtual {
        underlying.safeTransfer(to, amount);
    }

    /* ACCOUNTING FUNCTIONS
     *****************************************************************************************************************/

    /// @notice Calculates the amount of underlying tokens that would be exchanged for a given amount of principal tokens.
    // @notice 计算将为给定数量的本金代币交换的基础代币的数量。
    /// @dev Before maturity, it converts to underlying as if at maturity.
    // @dev 到期之前,它会转换为基础,就像在到期时一样。
    /// @param principalAmount The amount principal on which to calculate conversion.
    // @param principalAmount 用于计算转换的金本数额。
    /// @return underlyingAmount The total amount of underlying that would be received for the given principal amount..
    // @return underlyingAmount 将为给定的本金数额收到的基础代币的总量。
    function convertToUnderlying(uint256 principalAmount) external view returns (uint256 underlyingAmount) {
        return _convertToUnderlying(principalAmount);
    }

    function _convertToUnderlying(uint256 principalAmount) internal view virtual returns (uint256 underlyingAmount) {
        return principalAmount;
    }

    /// @notice Converts a given amount of underlying tokens to principal exclusive of fees.
    // @notice 将给定数量的基础代币转换为本金,不包括费用。
    /// @dev Before maturity, it converts to principal as if at maturity.
    // @dev 到期之前,它会转换为本金,就像在到期时一样。
    /// @param underlyingAmount The total amount of underlying on which to calculate the conversion.
    // @param underlyingAmount 用于计算转换的基础代币的总量。
    /// @return principalAmount The amount principal tokens required to provide the given amount of underlying.
    // @return principalAmount 提供给定数量基础代币所需的本金代币的数量。
    function convertToPrincipal(uint256 underlyingAmount) external view returns (uint256 principalAmount) {
        return _convertToPrincipal(underlyingAmount);
    }

    function _convertToPrincipal(uint256 underlyingAmount) internal view virtual returns (uint256 principalAmount) {
        return underlyingAmount;
    }

    /// @notice Allows user to simulate redemption of a given amount of principal tokens, inclusive of fees and other
    // @notice 允许用户模拟赎回给定数量的本金代币,包括费用和其他
    /// current block conditions.
    // 当前区块条件。
    /// @dev This reverts if before maturity.
    // @dev 如果在到期之前,则会回退。
    /// @param principalAmount The amount of principal that would be redeemed.
    // @param principalAmount 将被赎回的本金的数量。
    /// @return underlyingAmount The amount of underlying that would be received.
    // @return underlyingAmount 将收到的基础代币的数量。
    function previewRedeem(uint256 principalAmount) external view afterMaturity returns (uint256 underlyingAmount) {
        return _previewRedeem(principalAmount);
    }

    function _previewRedeem(uint256 principalAmount) internal view virtual returns (uint256 underlyingAmount) {
        return _convertToUnderlying(principalAmount); // should include fees/slippage
        // 应包括费用/滑点
    }

    /// @notice Calculates the maximum amount of principal tokens that an owner could redeem.
    // @notice 计算所有者可以赎回的最大本金代币数量。
    /// @dev This returns 0 if before maturity.
    // @dev 如果在到期之前,则返回 0。
    /// @param owner The address for which the redemption is being calculated.
    // @param owner 正在计算赎回的地址。
    /// @return maxPrincipalAmount The maximum amount of principal tokens that can be redeemed by the given owner.
    // @return maxPrincipalAmount 给定所有者可以赎回的最大本金代币数量。
    function maxRedeem(address owner) public view returns (uint256 maxPrincipalAmount) {
        return block.timestamp >= maturity ? _balanceOf[owner] : 0;
    }

    /// @notice Allows user to simulate withdraw of a given amount of underlying tokens.
    // @notice 允许用户模拟提取给定数量的基础代币。
    /// @dev This reverts if before maturity.
    // @dev 如果在到期之前,则会回退。
    /// @param underlyingAmount The amount of underlying tokens that would be withdrawn.
    // @param underlyingAmount 将被提取的基础代币的数量。
    /// @return principalAmount The amount of principal tokens that would be redeemed.
    // @return principalAmount 将被赎回的本金代币的数量。
    function previewWithdraw(uint256 underlyingAmount) external view afterMaturity returns (uint256 principalAmount) {
        return _previewWithdraw(underlyingAmount);
    }

    function _previewWithdraw(uint256 underlyingAmount) internal view virtual returns (uint256 principalAmount) {
        return _convertToPrincipal(underlyingAmount); // should include fees/slippage
        // 应包括费用/滑点
    }

    /// @notice Calculates the maximum amount of underlying tokens that can be withdrawn by a given owner.
    // @notice 计算给定所有者可以提取的最大基础代币数量。
    /// @dev This returns 0 if before maturity.
    // @dev 如果在到期之前,则返回 0。
    /// @param owner The address for which the withdraw is being calculated.
    // @param owner 正在计算提取的地址。
    /// @return maxUnderlyingAmount The maximum amount of underlying tokens that can be withdrawn by a given owner.
    // @return maxUnderlyingAmount 给定所有者可以提取的最大基础代币数量。
    function maxWithdraw(address owner) public view returns (uint256 maxUnderlyingAmount) {
        return _previewWithdraw(maxRedeem(owner));
    }
}

安全考虑

完全无需许可的用例可能会成为恶意实现的牺牲品,这些实现仅符合此 EIP 中的接口,但不符合规范,未能实现正确的托管功能,但提供了通过二级市场购买本金代币的能力。

建议所有集成商在集成之前审查每个实现,以了解可能导致用户存款损失的潜在方式。

convertToUnderlying 方法是一种有用的显示目的的估计值, 并且不必提供上下文建议的_确切_基础资产数量。

与许多标准一样,强烈建议尽可能地镜像底层代币的 decimals,以消除可能的混淆来源并简化跨前端和其他链下用户的集成。

版权

Copyright and related rights waived via CC0. 版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Julian Traversa (@JTraversa), Robert Robbins (@robrobbins), Alberto Cuesta Cañada (@alcueca), "ERC-5095: 本金代币 [DRAFT]," Ethereum Improvement Proposals, no. 5095, May 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5095.