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 |
Table of Contents
摘要
以下标准通过添加对异步存款和赎回流程的支持来扩展 ERC-4626。异步流程称为请求(Requests)。
添加了新方法来异步请求存款或赎回,并查看请求的状态。现有的 deposit
、mint
、withdraw
和 redeem
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):将请求更改为已认领状态的相应金库方法(例如,
deposit
或mint
从requestDeposit
中认领shares
)。小写的 claim 始终描述调用认领函数的动词操作。 - 异步存款(asynchronous deposit)金库:实现异步存款请求流程的金库
- 异步赎回(asynchronous redemption)金库:实现异步赎回请求流程的金库
- 完全异步(fully asynchronous)金库:同时实现异步存款和赎回请求的金库
- 控制者(controller):请求的所有者,可以管理与请求相关的任何操作,包括认领
assets
或shares
- 操作者(operator):可以代表另一个帐户管理请求的帐户。
请求流程
ERC-7540 金库 必须实现异步存款和赎回请求流程中的一个或两个。如果任一流程未在请求模式中实现,则必须使用 ERC-4626 标准同步交互模式。
所有 ERC-7540 异步代币化金库必须实现 ERC-4626,并覆盖下面描述的某些行为。
异步存款金库必须按如下方式覆盖 ERC-4626 规范:
deposit
和mint
方法不将assets
转移到金库,因为这已经在requestDeposit
上发生。previewDeposit
和previewMint
必须对所有调用者和输入都回退(revert)。
异步赎回金库必须按如下方式覆盖 ERC-4626 规范:
redeem
和withdraw
方法不将shares
转移到金库,因为这已经在requestRedeem
上发生。redeem
和withdraw
的owner
字段应该重命名为controller
,并且controller
必须是msg.sender
,除非controller
已批准msg.sender
作为操作者。previewRedeem
和previewWithdraw
必须对所有调用者和输入都回退(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* 和相应的认领函数,即使在同一区块中也是如此。金库不得在请求后将代币“推送”到用户,用户必须通过认领函数“提取”代币。
对于异步金库,shares
和 assets
之间的汇率(包括费用和收益)由金库实现决定。换句话说,待处理的赎回请求可能不计入收益,也可能没有固定的汇率。
请求 ID
请求的请求 ID(requestId
)由相应的 requestDeposit
和 requestRedeem
函数返回。
多个请求可能具有相同的 requestId
,因此给定的请求由 requestId
和 controller
共同区分。
具有相同 requestId
的请求必须彼此可替换(requestId == 0
这种特殊情况除外,如下所述)。即,所有具有相同 requestId
的请求必须同时从待处理状态转换为可认领状态,并获得相同的 assets
和 shares
之间的汇率。如果 requestId != 0
的请求变为部分可认领,则所有相同 requestId
的请求必须以相同的按比例费率变为可认领。
对于具有不同 requestId
的请求,没有假设或要求。即,它们可能在不同的时间转换为可认领状态,并且汇率也可能不同,没有任何排序或相关性以任何方式强制执行。
当 requestId==0
时,金库必须纯粹使用 controller
来区分请求状态。来自同一 controller
的多个请求的“待处理”和“可认领”状态将被聚合。如果金库为任何请求的 requestId
返回 0
,则必须为所有请求返回 0
。
方法
requestDeposit
将 assets
从 owner
转移到金库中,并提交一个请求以进行异步 deposit
。 这会将请求置于“待处理”状态,并且 pendingDepositRequest
对应增加 assets
金额。
输出 requestId
用于部分区分请求以及 controller
。 有关更多信息,请参见请求 ID部分。
当请求为“可认领”时,claimableDepositRequest
将为 controller
增加。 随后,controller
可以调用 deposit
或 mint
以接收 shares
。 请求可以直接转换为“可认领”状态,但不能跳过“可认领”状态。
在 deposit
或 mint
上收到的 shares
可能不等同于请求时 convertToShares(assets)
的值,因为价格会在“请求”和“认领”之间发生变化。
必须支持 ERC-20 approve
/ transferFrom
在 asset
上作为请求存款流程。
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
具有给定 requestId
的 controller
的待处理状态下的请求 assets
金额以 deposit
或 mint
。
对于 deposit
或 mint
,不得包括“可声明”状态下的任何 assets
。
不得显示任何取决于调用者的变化。
除非由于不合理的超大输入而导致整数溢出,否则不得回退。
- name: pendingDepositRequest
type: function
stateMutability: view
inputs:
- name: requestId
type: uint256
- name: controller
type: address
outputs:
- name: assets
type: uint256
claimableDepositRequest
具有给定 requestId
的 controller
的可认领状态下的请求 assets
金额以 deposit
或 mint
。
对于 deposit
或 mint
,不得包括“待处理”状态下的任何 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
不等于 owner
的 shares
的赎回请求批准,可能来自 ERC-20 批准通过 owner
的 shares
,或者如果 owner
已批准 msg.sender
作为操作员。 这必须与 ERC-6909 中指出的类似行为保持一致,在“批准和运营商”部分中:“根据 transferFrom 方法,具有运营商权限的消费方不受津贴限制,具有无限批准的消费方不应减少其委托转移的津贴,但是具有非无限批准的消费方必须减少其委托转移的余额。”
当请求为“可认领”时,claimableRedeemRequest
将为 controller
增加。 随后,controller
可以调用 redeem
或 withdraw
以接收 assets
。 请求可以直接转换为“可认领”状态,但不能跳过“可认领”状态。
在 redeem
或 withdraw
上收到的 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
具有给定 requestId
的 controller
的待处理状态下的请求 shares
金额以 redeem
或 withdraw
。
对于 redeem
或 withdraw
,不得包括“可声明”状态下的任何 shares
。
不得显示任何取决于调用者的变化。
除非由于不合理的超大输入而导致整数溢出,否则不得回退。
- name: pendingRedeemRequest
type: function
stateMutability: view
inputs:
- name: requestId
type: uint256
- name: controller
type: address
outputs:
- name: shares
type: uint256
claimableRedeemRequest
具有给定 requestId
的 controller
的可认领状态下的请求 shares
金额以 redeem
或 withdraw
。
对于 redeem
或 withdraw
,不得包括“待处理”状态下的任何 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
deposit
和 mint
重载方法
实现必须在 ERC-4626 的规范上支持一个附加的重载 deposit
和 mint
方法,该方法具有一个附加的 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
控制此请求。 sender
是 requestDeposit
的调用者,可能不等于 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-721 或 ERC-1155 的请求会造成太多的接口膨胀。
使用 ID 和地址来区分请求,允许在外部层开发任何这些用例,而不会给核心接口增加太多的复杂性。
某些金库,尤其是 requestId==0
的情况,得益于使用底层 ERC-4626 方法进行声明,因为在 requestId
级别上没有区分。 此标准主要针对这些用例而编写。 未来的标准可以优化非零请求 ID,并支持声明和传输请求,还可以通过 requestId
进行区分。
对称性以及不包括 requestWithdraw 和 requestMint
在 ERC-4626 中,编写该规范是为了在通过包括 deposit/withdraw 和 mint/redeem 来转换 assets
和 shares
方面具有完全的对称性。
由于请求的性质,异步金库只能对请求时完全知道的数量(deposit
的 assets
和 redeem
的 shares
)进行确定性操作。 因此,存款请求流不能与 mint
调用一起使用,因为在履行请求之前,所请求的 shares
数量的 assets
数量可能会波动。 同样,赎回请求流不能与 withdraw
调用一起使用。
流程的可选性
某些用例仅在存款或赎回请求流程的一侧是异步的。 异步赎回金库的一个很好的例子是流动性质押代币。 取消质押期间需要支持异步提款,但是存款可以是完全同步的。
不包括请求取消流程
在许多情况下,取消请求可能并非易事,甚至在技术上不可行。 取消的状态转换可以是同步的或异步的,而取消与剩余金库功能的交互方式非常复杂。
应该开发一个单独的 EIP 来标准化取消待处理请求的行为。 定义取消流程对于某些用例类别仍然很重要,对于这些用例,履行请求可能需要相当长的时间。
请求实现的灵活性
该标准足够灵活,可以支持各种请求流程的交互模式。 待处理的请求可以通过内部会计,全局或按用户级别来处理,请使用 ERC-20 或 ERC-721 等。
同样,是否可以累积赎回请求的收益以及任何请求的汇率可以是固定的或可变的,具体取决于实现。
不允许声明的短路
如果声明可以短路,则会给集成商带来歧义,并使接口与请求功能上的重载行为复杂化。
短路请求流程的示例如下:用户触发进入“待处理”状态的请求。 当金库履行请求时,相应的 assets/shares
将直接推送到用户。 这只需要用户执行 1 个步骤。
这种方法存在以下几个问题:
- 成本/缺乏可伸缩性:随着金库用户数量的增长,将声明成本转嫁给金库运营商可能会变得难以实现
- 阻碍了集成潜力:金库集成商将需要同时处理 2 步和 1 步的情况,其中 1 步从未知的请求在未知的时间推入任意代币。 这会将复杂性推向集成商,并降低标准的实用性。
通过使用路由器、中继器、消息签名或帐户抽象,可以将标准中使用的 2 步方法从用户的角度抽象为 1 步方法。
如果请求可以在同一区块中立即变为可声明,则可以有路由器合约在请求时立即原子性地检查可声明的数量。 前端可以根据金库的状态和实施方式以这种方式动态地路由请求,以处理此边缘情况。
请求函数没有输出
requestDeposit
和 requestRedeem
可能没有已知的汇率,该汇率会在请求变为 “已声明” 时发生。 在这种情况下,返回相应的 assets
或 shares
无法正常运行。
请求还可以输出一个时间戳,该时间戳表示请求预期变为 “已声明” 的最短时间,但是,并非所有金库都能返回可靠的时间戳。
没有可声明状态的事件
请求从“待处理”到“可声明”的状态转换发生在金库实施级别,并且未在标准中指定。 可以将请求分批处理为“可声明”状态,或者可以在时间戳通过后自动转换状态。 在用户或批处理级别上,没有必要在请求变为“可声明”之后发出事件。
异步请求流程中预览函数的回退
预览函数不采用地址参数,因此区分汇率差异的唯一方法是通过 msg.sender
。 但是,这可能会导致集成/实施复杂性,在这种情况下,支持合约无法代表 controller
确定声明的输出。
此外,预览声明步骤没有任何链上好处,因为唯一有效的状态转换是继续进行声明。 如果由于任何原因导致声明的输出不是所希望的,则调用合约可以回退该函数调用的输出。
简单地强制执行异步流程的预览函数的回退,可以减少代码和实施复杂性,而几乎没有成本。
强制支持 ERC-165
强制实施对 ERC-165 的支持,因为流程的可选性。 集成商可以使用 supportsInterface
方法来检查金库是完全异步的,部分异步的还是完全同步的(为此,它只是遵循 ERC-4626),并使用单个合约来支持所有情况。
不允许待处理的声明是可替换的
异步挂起的声明代表一种半可替代的中间份额类别。 金库可以选择将这些声明包装在他们喜欢的任何代币标准中,例如,ERC-20,ERC-1155 或 ERC-721,具体取决于用例。 这是故意遗漏在该规范之外的,以为实施者提供灵活性。
向后兼容性
该接口与 ERC-4626 完全向后兼容。 如规范中所述,deposit
,mint
,redeem
和 withdraw
方法的规范有所不同。
参考实现
// 此代码段是不完整的伪代码,仅用于示例,并非旨在用于生产或保证安全
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
的控制权。
任何批准操作员的用户都必须信任该操作员的金库的 asset
和 share
。
版权
版权和相关权利通过 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.