ERC-4626: Tokenized Vaults
Tokenized Vaults with a single underlying EIP-20 token.
Authors | Joey Santoro (@joeysantoro), t11s (@transmissions11), Jet Jadeja (@JetJadeja), Alberto Cuesta Cañada (@alcueca), Señor Doggo (@fubuloubu) |
---|---|
Created | 2021-12-22 |
Requires | EIP-20, EIP-2612 |
摘要
以下标准允许为代币化的 Vault 实现一个标准 API,这些 Vault 代表了单个底层 EIP-20 代币的份额。 此标准是对 EIP-20 代币的扩展,它为存入和提取代币以及读取余额提供了基本功能。
动机
代币化的 Vault 缺乏标准化,导致实现细节多样化。 一些不同的例子包括借贷市场、聚合器和内生计息代币。 这使得协议在聚合器或插件层面的集成变得困难,因为协议需要符合许多标准,并迫使每个协议实现自己的适配器,这些适配器容易出错并浪费开发资源。
代币化的 Vault 标准将降低计息 Vault 的集成工作量,同时创建更一致和健壮的实现模式。
规范
所有 EIP-4626 代币化的 Vault 必须实现 EIP-20 来表示份额。
如果 Vault 不可转移,它可能会在调用 transfer
或 transferFrom
时回退。
EIP-20 操作 balanceOf
、transfer
、totalSupply
等作用于 Vault 的“份额”,
这些份额代表对 Vault 底层资产的一部分所有权的声明。
所有 EIP-4626 代币化的 Vault 必须实现 EIP-20 的可选元数据扩展。
name
和 symbol
函数应该以某种方式反映底层代币的 name
和 symbol
。
EIP-4626 代币化的 Vault 可以实现 EIP-2612 以改善在各种集成上批准份额的 UX。
定义:
- asset: Vault 管理的底层代币。 具有由相应 EIP-20 合约定义的单位。
- share: Vault 的代币。具有在 mint/deposit/withdraw/redeem 时交换的底层资产的比率(由 Vault 定义)。
- fee: Vault 向用户收取的资产或份额金额。费用可以存在于存款、收益、AUM、提款或 Vault 规定的任何其他费用中。
- slippage: 公布的份额价格与存款或提款的经济现实之间的任何差异,该差异未由费用说明。
方法
asset
用于 Vault 进行核算、存款和取款的底层代币的地址。
必须是 EIP-20 代币合约。
必须_NOT_ 回退。
- name: asset
type: function
stateMutability: view
inputs: []
outputs:
- name: assetTokenAddress
type: address
totalAssets
Vault“管理”的底层资产的总量。
应该包括由收益产生的任何复利。
必须包括对 Vault 中资产收取的任何费用。
必须_NOT_ 回退。
- name: totalAssets
type: function
stateMutability: view
inputs: []
outputs:
- name: totalManagedAssets
type: uint256
convertToShares
在满足所有条件的情况下,Vault 将交换为所提供的资产数量的份额数量。
必须不包括对 Vault 中资产收取的任何费用。
不得显示任何取决于调用者的变化。
在执行实际交换时,不得反映滑点或其他链上条件。
除非由于过大的输入导致整数溢出,否则不得回退。
必须向下舍入到 0。
此计算可能不反映“每用户”的每股价格,而应反映“平均用户”的每股价格,这意味着平均用户在进行双向交换时应该看到的价格。
- name: convertToShares
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
convertToAssets
在满足所有条件的情况下,Vault 将交换为所提供的份额数量的资产数量。
必须不包括对 Vault 中资产收取的任何费用。
不得显示任何取决于调用者的变化。
在执行实际交换时,不得反映滑点或其他链上条件。
除非由于过大的输入导致整数溢出,否则不得回退。
必须向下舍入到 0。
此计算可能不反映“每用户”的每股价格,而应反映“平均用户”的每股价格,这意味着平均用户在进行双向交换时应该看到的价格。
- name: convertToAssets
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
maxDeposit
可以通过 deposit
调用存入到 Vault 的底层资产的最大数量,针对 receiver
。
必须返回 deposit
允许为 receiver
存入的最大资产数量,并且不会导致回退,该数量不得高于将被接受的实际最大值(如有必要,应该低估)。这假设用户拥有无限的资产,即必须不依赖 asset
的 balanceOf
。
必须考虑全局和用户特定的限制,例如如果完全禁用存款(即使是暂时的),它必须返回 0。
如果可以存入的最大资产数量没有限制,则必须返回 2 ** 256 - 1
。
必须_NOT_ 回退。
- name: maxDeposit
type: function
stateMutability: view
inputs:
- name: receiver
type: address
outputs:
- name: maxAssets
type: uint256
previewDeposit
允许链上或链下用户在当前区块模拟其存款的效果,给定当前的链上条件。
必须返回与在同一交易中 deposit
调用中铸造的 Vault 份额的精确数量尽可能接近且不大于该数量。即,如果在同一交易中调用,deposit
应该返回与 previewDeposit
相同或更多的 shares
。
必须不考虑像从 maxDeposit 返回的存款限制,并且应该始终表现得好像存款将被接受,无论用户是否有足够的代币批准等。
必须包括存款费用。集成商应该意识到存款费用的存在。
不得因 vault 特定的用户/全局限制而回退。可能因其他也会导致 deposit
回退的条件而回退。
请注意,convertToShares
和 previewDeposit
之间的任何不利差异都应被视为份额价格的滑点或某种其他类型的条件,这意味着存款人将因存款而损失资产。
- name: previewDeposit
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
deposit
通过存入确切的 assets
个底层代币,向 receiver
铸造 shares
个 Vault 份额。
必须发出 Deposit
事件。
必须支持 EIP-20 approve
/ transferFrom
在 asset
上作为存款流程。
可以支持另一个流程,其中底层代币在 deposit
执行之前由 Vault 合约拥有,并在 deposit
期间进行核算。
如果所有 assets
无法存入(由于达到存款限制、滑点、用户未向 Vault 合约批准足够的底层代币等),则必须回退。
请注意,大多数实现将需要预先批准 Vault 的底层 asset
代币。
- name: deposit
type: function
stateMutability: nonpayable
inputs:
- name: assets
type: uint256
- name: receiver
type: address
outputs:
- name: shares
type: uint256
maxMint
可以通过 mint
调用从 Vault 中为 receiver
铸造的最大份额数量。
必须返回 mint
允许存入到 receiver
的最大份额数量,并且不会导致回退,该数量不得高于将被接受的实际最大值(如有必要,应该低估)。这假设用户拥有无限的资产,即必须不依赖 asset
的 balanceOf
。
必须考虑全局和用户特定的限制,例如如果完全禁用铸造(即使是暂时的),它必须返回 0。
如果可以铸造的最大份额数量没有限制,则必须返回 2 ** 256 - 1
。
必须_NOT_ 回退。
- name: maxMint
type: function
stateMutability: view
inputs:
- name: receiver
type: address
outputs:
- name: maxShares
type: uint256
previewMint
允许链上或链下用户在当前区块模拟其铸造的效果,给定当前的链上条件。
必须返回与在同一交易中 mint
调用中存入的资产的精确数量尽可能接近且不小于该数量。即,如果在同一交易中调用, mint
应该返回与 previewMint
相同或更少的 assets
。
必须不考虑像从 maxMint 返回的铸造限制,并且应该始终表现得好像铸造将被接受,无论用户是否有足够的代币批准等。
必须包括存款费用。集成商应该意识到存款费用的存在。
不得因 vault 特定的用户/全局限制而回退。可能因其他也会导致 mint
回退的条件而回退。
请注意,convertToAssets
和 previewMint
之间的任何不利差异都应被视为份额价格的滑点或某种其他类型的条件,这意味着存款人将因铸造而损失资产。
- name: previewMint
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
mint
通过存入 assets
个底层代币,向 receiver
铸造确切的 shares
个 Vault 份额。
必须发出 Deposit
事件。
必须支持 EIP-20 approve
/ transferFrom
在 asset
上作为铸造流程。
可以支持另一个流程,其中底层代币在 mint
执行之前由 Vault 合约拥有,并在 mint
期间进行核算。
如果所有 shares
无法铸造(由于达到存款限制、滑点、用户未向 Vault 合约批准足够的底层代币等),则必须回退。
请注意,大多数实现将需要预先批准 Vault 的底层 asset
代币。
- name: mint
type: function
stateMutability: nonpayable
inputs:
- name: shares
type: uint256
- name: receiver
type: address
outputs:
- name: assets
type: uint256
maxWithdraw
可以通过 withdraw
调用从 Vault 中的 owner
余额中提取的底层资产的最大数量。
必须返回可以通过 withdraw
从 owner
转移的最大资产数量,并且不会导致回退,该数量不得高于将被接受的实际最大值(如有必要,应该低估)。
必须考虑全局和用户特定的限制,例如如果完全禁用提款(即使是暂时的),它必须返回 0。
必须_NOT_ 回退。
- name: maxWithdraw
type: function
stateMutability: view
inputs:
- name: owner
type: address
outputs:
- name: maxAssets
type: uint256
previewWithdraw
允许链上或链下用户在当前区块模拟其提款的效果,给定当前的链上条件。
必须返回与在同一交易中 withdraw
调用中销毁的 Vault 份额的精确数量尽可能接近且不小于该数量。即,如果在同一交易中调用, withdraw
应该返回与 previewWithdraw
相同或更少的 shares
。
必须不考虑像从 maxWithdraw 返回的提款限制,并且应该始终表现得好像提款将被接受,无论用户是否有足够的份额等。
必须包括提款费用。集成商应该意识到提款费用的存在。
不得因 vault 特定的用户/全局限制而回退。可能因其他也会导致 withdraw
回退的条件而回退。
请注意,convertToShares
和 previewWithdraw
之间的任何不利差异都应被视为份额价格的滑点或某种其他类型的条件,这意味着存款人将因存款而损失资产。
- name: previewWithdraw
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
withdraw
从 owner
处销毁 shares
,并将确切的 assets
个底层代币发送到 receiver
。
必须发出 Withdraw
事件。
必须支持一种提款流程,其中份额直接从 owner
处销毁,其中 owner
是 msg.sender
。
必须支持一种提款流程,其中份额直接从 owner
处销毁,其中 msg.sender
具有对 owner
份额的 EIP-20 批准。
可以支持另一个流程,其中份额在 withdraw
执行之前转移到 Vault 合约,并在 withdraw
期间进行核算。
应该检查 msg.sender
是否可以使用 owner 资金,资产需要转换为份额,并且应该检查份额的授权。
如果所有 assets
无法提取(由于达到提款限制、滑点、所有者没有足够的份额等),则必须回退。
请注意,某些实现将需要在执行提款之前预先请求 Vault。这些方法应该单独执行。
- name: withdraw
type: function
stateMutability: nonpayable
inputs:
- name: assets
type: uint256
- name: receiver
type: address
- name: owner
type: address
outputs:
- name: shares
type: uint256
maxRedeem
可以通过 redeem
调用从 Vault 中的 owner
余额中赎回的最大 Vault 份额数量。
必须返回可以通过 redeem
从 owner
转移的最大份额数量,并且不会导致回退,该数量不得高于将被接受的实际最大值(如有必要,应该低估)。
必须考虑全局和用户特定的限制,例如如果完全禁用赎回(即使是暂时的),它必须返回 0。
必须_NOT_ 回退。
- name: maxRedeem
type: function
stateMutability: view
inputs:
- name: owner
type: address
outputs:
- name: maxShares
type: uint256
previewRedeem
允许链上或链下用户在当前区块模拟其赎回的效果,给定当前的链上条件。
必须返回与在同一交易中 redeem
调用中提取的资产的精确数量尽可能接近且不大于该数量。即,如果在同一交易中调用, redeem
应该返回与 previewRedeem
相同或更多的 assets
。
必须不考虑像从 maxRedeem 返回的赎回限制,并且应该始终表现得好像赎回将被接受,无论用户是否有足够的份额等。
必须包括提款费用。集成商应该意识到提款费用的存在。
不得因 vault 特定的用户/全局限制而回退。可能因其他也会导致 redeem
回退的条件而回退。
请注意,convertToAssets
和 previewRedeem
之间的任何不利差异都应被视为份额价格的滑点或某种其他类型的条件,这意味着存款人将因赎回而损失资产。
- name: previewRedeem
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
redeem
从 owner
处销毁确切的 shares
,并将 assets
个底层代币发送到 receiver
。
必须发出 Withdraw
事件。
必须支持一种赎回流程,其中份额直接从 owner
处销毁,其中 owner
是 msg.sender
。
必须支持一种赎回流程,其中份额直接从 owner
处销毁,其中 msg.sender
具有对 owner
份额的 EIP-20 批准。
可以支持另一个流程,其中份额在 redeem
执行之前转移到 Vault 合约,并在 redeem
期间进行核算。
应该检查 msg.sender
是否可以使用授权使用所有者的资金。
如果所有 shares
无法赎回(由于达到提款限制、滑点、所有者没有足够的份额等),则必须回退。
请注意,某些实现将需要在执行提款之前预先请求 Vault。这些方法应该单独执行。
- name: redeem
type: function
stateMutability: nonpayable
inputs:
- name: shares
type: uint256
- name: receiver
type: address
- name: owner
type: address
outputs:
- name: assets
type: uint256
事件
Deposit
sender
已经将 assets
交换为 shares
,并将这些 shares
转移到 owner
。
当通过 mint
和 deposit
方法将代币存入 Vault 时,必须发出此事件。
- name: Deposit
type: event
inputs:
- name: sender
indexed: true
type: address
- name: owner
indexed: true
type: address
- name: assets
indexed: false
type: uint256
- name: shares
indexed: false
type: uint256
Withdraw
sender
已经将 owner
拥有的 shares
交换为 assets
,并将这些 assets
转移到 receiver
。
当通过 EIP-4626.redeem
或 EIP-4626.withdraw
方法从 Vault 中提取份额时,必须发出此事件。
- name: Withdraw
type: event
inputs:
- name: sender
indexed: true
type: address
- name: receiver
indexed: true
type: address
- name: owner
indexed: true
type: address
- name: assets
indexed: false
type: uint256
- name: shares
indexed: false
type: uint256
理由
Vault 接口旨在针对具有完整功能但最小接口的集成商进行优化。 诸如会计和已存款代币的分配等细节有意未指定, 因为预计 Vault 在链上被视为黑盒,并在使用前在链下进行检查。
强制执行 EIP-20 是因为诸如代币批准和余额计算之类的实现细节直接延续到份额会计。 这种标准化使 Vault 除了 EIP-4626 之外,还可以立即与所有 EIP-20 用例兼容。
包含 mint 方法是为了对称性和特征完整性。
当前基于份额的 Vault 的大多数用例都没有将特殊含义赋予份额,因此
用户将优化特定数量的份额 (mint
),而不是特定数量的底层资产 (deposit
)。
但是,很容易想象未来的 Vault 策略,这些策略将具有独特且独立有用的份额表示形式。
convertTo
函数充当粗略的估计,不考虑特定于操作的细节,例如提款费用等。
它们包含在需要份额或资产平均值的前端和应用程序中,而不是可能包括滑点或其他费用的确切值。
对于需要尝试考虑费用和滑点的确切值的应用程序,我们包含了相应的 preview
函数以匹配每个可变函数。这些函数不得考虑存款或提款限制,以确保它们易于组合,为此目的提供了 max
函数。
向后兼容性
EIP-4626 与 EIP-20 标准完全向后兼容,并且与其他标准没有已知的兼容性问题。 对于不使用 EIP-4626 的 Vault 的生产实现,可以开发和使用包装器适配器。
参考实现
请参阅 Solmate EIP-4626: 这是该标准的最小且有主见的实现,带有允许开发人员轻松将自定义逻辑插入存款和取款的钩子。
请参阅 Vyper EIP-4626: 这是 Vyper 中该标准的演示实现,带有用于份额价格操纵和其他测试需求的钩子。
安全注意事项
完全无需许可的用例可能会成为仅符合接口而不符合规范的恶意实现的牺牲品。 建议所有集成商在集成之前审查实现,以了解可能导致用户存款损失的潜在方式。
如果实施者打算直接支持 EOA 帐户访问,则他们应考虑为 deposit
/ mint
/ withdraw
/ redeem
添加一个额外的函数调用,并具有适应滑点损失或意外的存款/提款限制的方法,因为如果未达到确切的输出金额,它们没有其他方法可以恢复交易。
方法 totalAssets
,convertToShares
和 convertToAssets
是用于显示目的的估计值,
并且 不必_赋予与其上下文相关的 _确切 底层资产数量。
preview
方法返回的值尽可能接近精确。因此,它们可以通过更改链上条件进行操作,并且并非始终可以安全地用作价格预言机。此规范包括允许不精确的 convert
方法,因此可以实现为鲁棒的价格预言机。例如,在资产和份额之间转换时,将 convert
方法实现为使用时间加权平均价格是正确的。
EIP-4626 Vault 的集成商在与此标准集成时应注意这些视图方法之间的差异。此外,请注意,用户从赎回其 Vault 份额 (previewRedeem
) 可能收到的底层资产数量可能与铸造相同数量的份额 (previewMint
) 时从他们那里获得的数量显着不同。这些差异可能很小(例如由于舍入错误),或者非常显着(例如,如果 Vault 实施提款或存款费用等)。因此,集成商应始终注意使用与其用例最相关的预览函数,并且切勿假设它们可以互换。
最后,EIP-4626 Vault 实现者应注意在不同的可变和视图方法中需要特定的、相反的舍入方向,因为在计算过程中优先考虑 Vault 本身而不是用户被认为是最安全的:
-
如果 (1) 它正在计算要为用户提供的特定数量的底层代币发行多少份额,或者 (2) 它正在确定要转移给他们的底层代币数量以返回特定数量的份额,它应该向下_舍入_。
-
如果 (1) 它正在计算用户必须提供多少份额才能收到给定数量的底层代币,或者 (2) 它正在计算用户必须提供多少底层代币才能收到一定数量的份额,它应该向上_舍入_。
首选舍入方向不明确的唯一函数是 convertTo
函数。为了确保所有 EIP-4626 Vault 实现之间的一致性,指定这些函数必须始终向下_舍入_。积分器可能希望自己模仿这些函数的向上舍入版本,例如通过在结果中添加 1 wei。
虽然 convertTo
函数应消除对任何使用 EIP-4626 Vault 的 decimals
变量的需求,但仍强烈建议尽可能地镜像底层代币的 decimals
,以消除可能的混淆来源并简化跨前端和其他链下用户的集成。
版权
版权和相关权利通过 CC0 放弃。
Citation
Please cite this document as:
Joey Santoro (@joeysantoro), t11s (@transmissions11), Jet Jadeja (@JetJadeja), Alberto Cuesta Cañada (@alcueca), Señor Doggo (@fubuloubu), "ERC-4626: Tokenized Vaults," Ethereum Improvement Proposals, no. 4626, December 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4626.