Alert Source Discuss
Standards Track: ERC

ERC-7540: 异步 ERC-4626 代币化金库

通过异步存款和赎回支持扩展 ERC-4626

Authors Jeroen Offerijns (@hieronx), Alina Sinelnikova (@ilinzweilin), Vikram Arun (@vikramarun), Joey Santoro (@joeysantoro), Farhaan Ali (@0xfarhaan), João Martins (@0xTimepunk)
Created 2023-10-18
Requires EIP-20, EIP-165, EIP-4626, EIP-7575

摘要

以下标准通过添加对异步存款和赎回流程的支持来扩展 ERC-4626。异步流程称为请求(Requests)。

添加了新方法来异步请求存款或赎回,并查看请求的状态。现有的 depositmintwithdrawredeem ERC-4626 方法用于执行可认领请求(Claimable Requests)。

实现可以选择是否为存款、赎回或两者都添加异步流程。

动机

ERC-4626 代币化金库标准已帮助使生息代币在去中心化金融中更具可组合性。该标准针对高达限制的原子存款和赎回进行了优化。如果达到限制,则无法提交新的存款或赎回。

对于任何具有异步操作或延迟的智能合约系统,该限制都不太有效,这些系统是与金库交互的先决条件(例如,现实世界资产协议、抵押不足的借贷协议、跨链借贷协议、流动性质押代币或保险安全模块)。

此标准扩展了 ERC-4626 金库在异步用例中的实用性。现有的金库接口(deposit/withdraw/mint/redeem)已得到充分利用,以认领异步请求。

规范

定义:

ERC-4626 中的现有定义适用。此外,本规范定义:

  • 请求(Request):进入(requestDeposit)或退出(requestRedeem)金库的请求
  • 待处理(Pending):发出请求但尚未可认领的状态
  • 可认领(Claimable):请求已由金库处理的状态,使用户能够认领相应的 shares(用于异步存款)或 assets(用于异步赎回)
  • 已认领(Claimed):请求已由用户完成且用户收到输出代币的状态(例如,deposit 请求的 shares
  • 认领函数(Claim function):将请求更改为已认领状态的相应金库方法(例如,depositmintrequestDeposit 中认领 shares)。小写的 claim 始终描述调用认领函数的动词操作。
  • 异步存款(asynchronous deposit)金库:实现异步存款请求流程的金库
  • 异步赎回(asynchronous redemption)金库:实现异步赎回请求流程的金库
  • 完全异步(fully asynchronous)金库:同时实现异步存款和赎回请求的金库
  • 控制者(controller):请求的所有者,可以管理与请求相关的任何操作,包括认领 assetsshares
  • 操作者(operator):可以代表另一个帐户管理请求的帐户。

请求流程

ERC-7540 金库 必须实现异步存款和赎回请求流程中的一个或两个。如果任一流程未在请求模式中实现,则必须使用 ERC-4626 标准同步交互模式。

所有 ERC-7540 异步代币化金库必须实现 ERC-4626,并覆盖下面描述的某些行为。

异步存款金库必须按如下方式覆盖 ERC-4626 规范:

  1. depositmint 方法不将 assets 转移到金库,因为这已经在 requestDeposit 上发生。
  2. previewDepositpreviewMint 必须对所有调用者和输入都回退(revert)。

异步赎回金库必须按如下方式覆盖 ERC-4626 规范:

  1. redeemwithdraw 方法不将 shares 转移到金库,因为这已经在 requestRedeem 上发生。
  2. redeemwithdrawowner 字段应该重命名为 controller,并且 controller 必须是 msg.sender,除非 controller 已批准 msg.sender 作为操作者。
  3. previewRedeempreviewWithdraw 必须对所有调用者和输入都回退(revert)。

请求生命周期

提交后,请求将经历“待处理”、“可认领”和“已认领”阶段。下表可视化了存款请求的生命周期示例。

状态 用户 金库
待处理 requestDeposit(assets, controller, owner) asset.transferFrom(owner, vault, assets); pendingDepositRequest[controller] += assets
可认领   内部请求履行pendingDepositRequest[controller] -= assets; claimableDepositRequest[controller] += assets
已认领 deposit(assets, receiver, controller) claimableDepositRequest[controller] -= assets; vault.balanceOf[receiver] += shares

请注意,maxDeposit 的增加和减少与 claimableDepositRequest 同步。

请求不得跳过或以其他方式绕过认领状态。换句话说,要启动和认领请求,用户必须分别调用 request* 和相应的认领函数,即使在同一区块中也是如此。金库不得在请求后将代币“推送”到用户,用户必须通过认领函数“提取”代币。

对于异步金库,sharesassets 之间的汇率(包括费用和收益)由金库实现决定。换句话说,待处理的赎回请求可能不计入收益,也可能没有固定的汇率。

请求 ID

请求的请求 ID(requestId)由相应的 requestDepositrequestRedeem 函数返回。

多个请求可能具有相同的 requestId,因此给定的请求由 requestIdcontroller 共同区分。

具有相同 requestId 的请求必须彼此可替换(requestId == 0 这种特殊情况除外,如下所述)。即,所有具有相同 requestId 的请求必须同时从待处理状态转换为可认领状态,并获得相同的 assetsshares 之间的汇率。如果 requestId != 0 的请求变为部分可认领,则所有相同 requestId 的请求必须以相同的按比例费率变为可认领。

对于具有不同 requestId 的请求,没有假设或要求。即,它们可能在不同的时间转换为可认领状态,并且汇率也可能不同,没有任何排序或相关性以任何方式强制执行。

requestId==0 时,金库必须纯粹使用 controller 来区分请求状态。来自同一 controller 的多个请求的“待处理”和“可认领”状态将被聚合。如果金库为任何请求的 requestId 返回 0,则必须为所有请求返回 0

方法

requestDeposit

assetsowner 转移到金库中,并提交一个请求以进行异步 deposit。 这会将请求置于“待处理”状态,并且 pendingDepositRequest 对应增加 assets 金额。

输出 requestId 用于部分区分请求以及 controller。 有关更多信息,请参见请求 ID部分。

当请求为“可认领”时,claimableDepositRequest 将为 controller 增加。 随后,controller 可以调用 depositmint 以接收 shares。 请求可以直接转换为“可认领”状态,但不能跳过“可认领”状态。

depositmint 上收到的 shares 可能不等同于请求时 convertToShares(assets) 的值,因为价格会在“请求”和“认领”之间发生变化。

必须支持 ERC-20 approve / transferFromasset 上作为请求存款流程。

owner 必须等于 msg.sender,除非 owner 已批准 msg.sender 作为操作者。

如果并非所有 assets 都不能被请求以进行 deposit / mint,则必须回退(由于达到了存款限额,滑点,用户未批准足够的底层代币到金库合约等)。

请注意,大多数实现都需要预先使用金库的底层 asset 代币批准金库。

必须发出 DepositRequest 事件。

- name: requestDeposit
  type: function
  stateMutability: nonpayable

  inputs:
    - name: assets
      type: uint256
    - name: controller
      type: address
    - name: owner
      type: address
  outputs:
    - name: requestId
      type: uint256

pendingDepositRequest

具有给定 requestIdcontroller 的待处理状态下的请求 assets 金额以 depositmint

对于 depositmint,不得包括“可声明”状态下的任何 assets

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

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

- name: pendingDepositRequest
  type: function
  stateMutability: view

  inputs:
    - name: requestId
      type: uint256
    - name: controller
      type: address

  outputs:
    - name: assets
      type: uint256

claimableDepositRequest

具有给定 requestIdcontroller 的可认领状态下的请求 assets 金额以 depositmint

对于 depositmint,不得包括“待处理”状态下的任何 assets

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

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

- name: claimableDepositRequest
  type: function
  stateMutability: view

  inputs:
    - name: requestId
      type: uint256
    - name: controller
      type: address

  outputs:
    - name: assets
      type: uint256

requestRedeem

owner 那里获取 shares 的控制权,并提交以进行异步 redeem 的请求。 这会将请求置于“待处理”状态,并且 pendingRedeemRequest 对应增加 shares 金额。

输出 requestId 用于区分请求以及 controller。 有关更多信息,请参见请求 ID部分。

为了进行会计处理,可以用在金库存临时锁定 shares,直到可声明或已声明状态为止,或者可以在 requestRedeem 时立即将其烧毁。

无论哪种情况,shares 都必须在 requestRedeem 时从 owner 的保管中删除,并在请求被声明时将其烧毁。

对于 msg.sender 不等于 ownershares 的赎回请求批准,可能来自 ERC-20 批准通过 ownershares,或者如果 owner 已批准 msg.sender 作为操作员。 这必须与 ERC-6909 中指出的类似行为保持一致,在“批准和运营商”部分中:“根据 transferFrom 方法,具有运营商权限的消费方不受津贴限制,具有无限批准的消费方不应减少其委托转移的津贴,但是具有非无限批准的消费方必须减少其委托转移的余额。”

当请求为“可认领”时,claimableRedeemRequest 将为 controller 增加。 随后,controller 可以调用 redeemwithdraw 以接收 assets。 请求可以直接转换为“可认领”状态,但不能跳过“可认领”状态。

redeemwithdraw 上收到的 assets 可能不等同于请求时 convertToAssets(shares) 的值,因为价格会在“待处理”和“已声明”之间发生变化。

如果并非所有 shares 都不能被请求以进行 redeem / withdraw,则必须回退(由于达到了提款限额,滑点,所有者没有足够的份额等)。

必须发出 RedeemRequest 事件。

- name: requestRedeem
  type: function
  stateMutability: nonpayable

  inputs:
    - name: shares
      type: uint256
    - name: controller
      type: address
    - name: owner
      type: address
  outputs:
    - name: requestId
    - type: uint256

pendingRedeemRequest

具有给定 requestIdcontroller 的待处理状态下的请求 shares 金额以 redeemwithdraw

对于 redeemwithdraw,不得包括“可声明”状态下的任何 shares

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

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

- name: pendingRedeemRequest
  type: function
  stateMutability: view

  inputs:
    - name: requestId
      type: uint256
    - name: controller
      type: address

  outputs:
    - name: shares
      type: uint256

claimableRedeemRequest

具有给定 requestIdcontroller 的可认领状态下的请求 shares 金额以 redeemwithdraw

对于 redeemwithdraw,不得包括“待处理”状态下的任何 shares

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

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

- name: claimableRedeemRequest
  type: function
  stateMutability: view

  inputs:
    - name: requestId
      type: uint256
    - name: controller
      type: address

  outputs:
    - name: shares
      type: uint256

isOperator

如果 operator 被批准为 controller 的操作员,则返回 true

- name: isOperator
  type: function
  stateMutability: view

  inputs:
    - name: controller
      type: address
    - name: operator
      type: address

  outputs:
    - name: status
      type: bool

setOperator

授予或撤销 operator 代表 msg.sender 管理请求的权限。

必须将操作员状态设置为 approved 值。

必须记录 OperatorSet 事件。

必须返回 True。

- name: setOperator
  type: function
  stateMutability: nonpayable

  inputs:
    - name: operator
      type: address
    - name: approved
      type: bool

  outputs:
    - name: success
      type: bool

depositmint 重载方法

实现必须在 ERC-4626 的规范上支持一个附加的重载 depositmint 方法,该方法具有一个附加的 controller 输入(类型为 address):

  • deposit(uint256 assets, address receiver, address controller)
  • mint(uint256 shares, address receiver, address controller)

除非 msg.sender 等于 controller 或是由 controller 批准的操作员,否则调用必须回退。

controller 字段用于区分应为 assets 声明的请求,以防 msg.sender 不是 controller

当发出 Deposit 事件时,第一个参数必须是 controller,第二个参数必须是 receiver

事件

DepositRequest

owner 已在金库中锁定 assets,以使用请求 ID requestId 请求存款。 controller 控制此请求。 senderrequestDeposit 的调用者,可能不等于 owner

当使用 requestDeposit 方法提交存款请求时,必须发出。

- name: DepositRequest
  type: event

  inputs:
    - name: controller
      indexed: true
      type: address
    - name: owner
      indexed: true
      type: address
    - name: requestId
      indexed: true
      type: uint256
    - name: sender
      indexed: false
      type: address
    - name: assets
      indexed: false
      type: uint256

RedeemRequest

sender 已在金库中锁定 shares(由 owner 拥有)以请求赎回。 controller 控制此请求,但不一定是 owner

当使用 requestRedeem 方法提交赎回请求时,必须发出。

- name: RedeemRequest
  type: event

  inputs:
    - name: controller
      indexed: true
      type: address
    - name: owner
      indexed: true
      type: address
    - name: requestId
      indexed: true
      type: uint256
    - name: sender
      indexed: false
      type: address
    - name: shares
      indexed: false
      type: uint256

OperatorSet

controller 已将 approved 状态设置为 operator

设置操作员状态时,必须记录。

将操作员状态设置为与当前调用之前的状态相同的状态时,可能会记录。

- name: OperatorSet
  type: event

  inputs:
    - name: controller
      indexed: true
      type: address
    - name: operator
      indexed: true
      type: address
    - name: approved
      indexed: false
      type: bool

ERC-165 支持

实施此金库标准的智能合约必须实施 ERC-165 supportsInterface 函数。

如果 0xe3bc4e65 (代表所有 ERC-7540 金库都实施的操作员方法)或 0x2f0a18c5(代表 ERC-7575 接口)通过 interfaceID 参数传递,则所有异步金库必须返回常量值 true

如果 0xce3bbe50 通过 interfaceID 参数传递,则异步存款金库必须返回常量值 true

如果 0x620ee8e4 通过 interfaceID 参数传递,则异步赎回金库必须返回常量值 true

ERC-7575 支持

实施此金库标准的智能合约必须实施 ERC-7575 标准(特别是 share 方法)。

理由

包括请求 ID 但不包括按 ID 声明的方法

由于其异步性,异步金库中的请求具有 NFT 或半同质化代币的属性。 但是,尝试将所有 ERC-7540 金库都归类为支持 ERC-721ERC-1155 的请求会造成太多的接口膨胀。

使用 ID 和地址来区分请求,允许在外部层开发任何这些用例,而不会给核心接口增加太多的复杂性。

某些金库,尤其是 requestId==0 的情况,得益于使用底层 ERC-4626 方法进行声明,因为在 requestId 级别上没有区分。 此标准主要针对这些用例而编写。 未来的标准可以优化非零请求 ID,并支持声明和传输请求,还可以通过 requestId 进行区分。

对称性以及不包括 requestWithdraw 和 requestMint

在 ERC-4626 中,编写该规范是为了在通过包括 deposit/withdraw 和 mint/redeem 来转换 assetsshares 方面具有完全的对称性。

由于请求的性质,异步金库只能对请求时完全知道的数量(depositassetsredeemshares)进行确定性操作。 因此,存款请求流不能与 mint 调用一起使用,因为在履行请求之前,所请求的 shares 数量的 assets 数量可能会波动。 同样,赎回请求流不能与 withdraw 调用一起使用。

流程的可选性

某些用例仅在存款或赎回请求流程的一侧是异步的。 异步赎回金库的一个很好的例子是流动性质押代币。 取消质押期间需要支持异步提款,但是存款可以是完全同步的。

不包括请求取消流程

在许多情况下,取消请求可能并非易事,甚至在技术上不可行。 取消的状态转换可以是同步的或异步的,而取消与剩余金库功能的交互方式非常复杂。

应该开发一个单独的 EIP 来标准化取消待处理请求的行为。 定义取消流程对于某些用例类别仍然很重要,对于这些用例,履行请求可能需要相当长的时间。

请求实现的灵活性

该标准足够灵活,可以支持各种请求流程的交互模式。 待处理的请求可以通过内部会计,全局或按用户级别来处理,请使用 ERC-20 或 ERC-721 等。

同样,是否可以累积赎回请求的收益以及任何请求的汇率可以是固定的或可变的,具体取决于实现。

不允许声明的短路

如果声明可以短路,则会给集成商带来歧义,并使接口与请求功能上的重载行为复杂化。

短路请求流程的示例如下:用户触发进入“待处理”状态的请求。 当金库履行请求时,相应的 assets/shares 将直接推送到用户。 这只需要用户执行 1 个步骤。

这种方法存在以下几个问题:

  • 成本/缺乏可伸缩性:随着金库用户数量的增长,将声明成本转嫁给金库运营商可能会变得难以实现
  • 阻碍了集成潜力:金库集成商将需要同时处理 2 步和 1 步的情况,其中 1 步从未知的请求在未知的时间推入任意代币。 这会将复杂性推向集成商,并降低标准的实用性。

通过使用路由器、中继器、消息签名或帐户抽象,可以将标准中使用的 2 步方法从用户的角度抽象为 1 步方法。

如果请求可以在同一区块中立即变为可声明,则可以有路由器合约在请求时立即原子性地检查可声明的数量。 前端可以根据金库的状态和实施方式以这种方式动态地路由请求,以处理此边缘情况。

请求函数没有输出

requestDepositrequestRedeem 可能没有已知的汇率,该汇率会在请求变为 “已声明” 时发生。 在这种情况下,返回相应的 assetsshares 无法正常运行。

请求还可以输出一个时间戳,该时间戳表示请求预期变为 “已声明” 的最短时间,但是,并非所有金库都能返回可靠的时间戳。

没有可声明状态的事件

请求从“待处理”到“可声明”的状态转换发生在金库实施级别,并且未在标准中指定。 可以将请求分批处理为“可声明”状态,或者可以在时间戳通过后自动转换状态。 在用户或批处理级别上,没有必要在请求变为“可声明”之后发出事件。

异步请求流程中预览函数的回退

预览函数不采用地址参数,因此区分汇率差异的唯一方法是通过 msg.sender。 但是,这可能会导致集成/实施复杂性,在这种情况下,支持合约无法代表 controller 确定声明的输出。

此外,预览声明步骤没有任何链上好处,因为唯一有效的状态转换是继续进行声明。 如果由于任何原因导致声明的输出不是所希望的,则调用合约可以回退该函数调用的输出。

简单地强制执行异步流程的预览函数的回退,可以减少代码和实施复杂性,而几乎没有成本。

强制支持 ERC-165

强制实施对 ERC-165 的支持,因为流程的可选性。 集成商可以使用 supportsInterface 方法来检查金库是完全异步的,部分异步的还是完全同步的(为此,它只是遵循 ERC-4626),并使用单个合约来支持所有情况。

不允许待处理的声明是可替换的

异步挂起的声明代表一种半可替代的中间份额类别。 金库可以选择将这些声明包装在他们喜欢的任何代币标准中,例如,ERC-20,ERC-1155 或 ERC-721,具体取决于用例。 这是故意遗漏在该规范之外的,以为实施者提供灵活性。

向后兼容性

该接口与 ERC-4626 完全向后兼容。 如规范中所述,depositmintredeemwithdraw 方法的规范有所不同。

参考实现

    // 此代码段是不完整的伪代码,仅用于示例,并非旨在用于生产或保证安全

    mapping(address => uint256) public pendingDepositRequest;
    
    mapping(address => uint256) public claimableDepositRequest;

    mapping(address controller => mapping(address operator => bool)) public isOperator;

    function requestDeposit(uint256 assets, address controller, address owner) external returns (uint256 requestId) {
        require(assets != 0);
        require(owner == msg.sender || isOperator[owner][msg.sender]);

        requestId = 0; // 没有与此请求关联的 requestId

        asset.safeTransferFrom(owner, address(this), assets); // asset 此处是金库底层资产

        pendingDepositRequest[controller] += assets;

        emit DepositRequest(controller, owner, requestId, msg.sender, assets);
        return requestId;
    }

    /**
     * 在此处包括一些从“待处理”到“可声明”的任意转换逻辑
     */

    function deposit(uint256 assets, address receiver, address controller) external returns (uint256 shares) {
        require(assets != 0);
        require(controller == msg.sender || isOperator[controller][msg.sender]);

        claimableDepositRequest[controller] -= assets; // 如果没有足够的可声明资产,则下溢将回退

        shares = convertToShares(assets); // 此幼稚的示例使用瞬时汇率。 使用在“可声明”阶段锁定的速率可能更常见。

        balanceOf[receiver] += shares;

        emit Deposit(controller, receiver, assets, shares);
    }

    function setOperator(address operator, bool approved) public returns (bool) {
        isOperator[msg.sender][operator] = approved;
        emit OperatorSet(msg.sender, operator, approved);
        return true;
    }

安全注意事项

通常,异步性问题使金库中的状态转换变得更加复杂,并且容易受到安全风险的影响。 应执行对金库操作的访问控制、状态转换的清晰文档和不变性检查,以减轻这些风险。 例如:

  • 用于查看“待处理”和“可认领”请求状态(例如,pendingDepositRequest)的查看方法是有用的估计值,可用于显示目的,但可能已过时。 无法知道任何请求的最终汇率,这要求用户信任异步金库在汇率计算和履行其请求中的实施。
  • 为请求锁定的份额或资产可能会卡在“待处理”状态。 金库可以选择允许待处理声明的可替代性,或实施某些取消功能以保护用户。

操作员

操作员能够将金库的 asset 从批准人转移到任何地址,并同时授予对金库share的控制权。

任何批准操作员的用户都必须信任该操作员的金库的 assetshare

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Jeroen Offerijns (@hieronx), Alina Sinelnikova (@ilinzweilin), Vikram Arun (@vikramarun), Joey Santoro (@joeysantoro), Farhaan Ali (@0xfarhaan), João Martins (@0xTimepunk), "ERC-7540: 异步 ERC-4626 代币化金库," Ethereum Improvement Proposals, no. 7540, October 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7540.