Euler金库套件

  • euler-xyz
  • 发布于 2025-01-14 12:30
  • 阅读 47

Euler Vault Kit (EVK) 是一个用于构建信用金库的系统,信用金库是具有额外借贷功能的 ERC-4626 金库。

<!-- END OF TOC -->

简介

Euler Vault Kit (EVK) 是一个用于构建信用 vault的系统。信用 vault 是具有额外借款功能的 ERC-4626 vault。与通过主动投资存入资金来赚取收益的典型 ERC-4626 vault 不同,信用 vault 是被动的借贷池。

只要用户在其他信用 vault 中存入足够的抵押品,就可以从信用 vault 中借款。负债 vault(借款的 vault)决定哪些信用 vault 可接受作为抵押品。通过不断增加其未偿还负债的金额向借款人收取利息,并且该利息为存款人带来收益。

Vault 与 Ethereum Vault Connector 合约 (EVC) 集成,该合约跟踪每个帐户用作抵押品的 vault。如果需要清算,EVC 允许负债 vault 代表用户提取抵押品。

EVC 也是与 vault 交互的替代入口点。它提供类似 multicall 的批处理、模拟、无 gas 交易和闪电流动性,以实现贷款的有效再融资。可以调用外部合约而无需特殊适配器,并且 EOA 和合约钱包都可以访问所有功能。尽管每个地址在任何给定时间只允许一个未偿还负债,但 EVC 为其提供 256 个虚拟地址,称为子帐户(从这里开始,简称为帐户)。子帐户地址是 EVC 和兼容 vault 内部的,应注意确保其他合约不使用这些地址。

EVC 负责身份验证,而 vault 负责授权。例如,如果用户尝试赎回一定数量的资产,EVC 确保请求实际上来自用户,并且 vault 确保用户实际上拥有此数量的资产。

创建

一个 vault 由几个通信组件组成:

  • 底层资产是将由 vault 持有的 ERC-20 token。每个 vault 仅持有一种底层资产。
  • EVault 是主要的入口点合约,并实现所有 vault 通用的逻辑,例如跟踪哪些地址已存款以及哪些地址有未偿还的借款、验证头寸的健康状况以及允许清算。出于代码组织的目的,它的一些逻辑被委托给静态(不可升级)模块
  • PriceOracle 组件与外部定价系统对接,以实时计算抵押品和负债的价值。
  • IRM(利率模型)组件计算利率以鼓励更多或更少的借款。
  • ProtocolConfig 是一个全局协议级别配置合约。此配置对 vault 没有直接影响,除了控制协议费用的目标地址,以及在协议和 vault 管理者之间分配的(有界)利息费用部分。

在创建 vault 之前,应创建(或重新使用)PriceOracleIRM 合约。这些将安装在 EVault 中。在创建期间,EVault 创建一个 side-car DToken 合约,以公开债务金额的只读 ERC-20 interface。

可升级 vs 不可变

EVault 实例由工厂合约创建。此工厂在其存储中具有一个 implementation 地址,该地址指向实际包含代码的合约。任何人都可以创建引用此实现合约的代理,每个代理都是一个 vault。创建代理时,指定一个布尔 upgradeable 标志:

  • 如果 upgradeable 为 true,则工厂将创建一个 beacon 代理,并将工厂本身设置为 beacon 合约。
  • 如果 upgradeable 为 false,则工厂将创建一个受 EIP-3448 MetaProxy 启发的最小代理合约,并将 implementation 的当前值作为目标合约。

工厂具有一个 upgradeAdmin 地址,该地址可以更改 implementation 的值,但这只会影响创建为 upgradeable 的 vault。这允许 vault 创建者选择是否希望工厂管理员能够升级他们的 vault,或者他们是否应该是不可变的。为了防止在验证之后但在 vault 创建之前更改实现,可以指定一个 desiredImplementation 参数。或者,可以在创建后确认 vault 的实现是所需的版本。

验证 vault 是由受信任的工厂创建的,可以验证 vault 的代码也可以被信任。用户应对未知工厂创建的 vault 进行尽职调查,因为它们可能是恶意的。用户界面可以选择仅显示由某些工厂创建的 vault。

受治理 vs 最终化

创建后,工厂将立即在代理上调用 initialize(),并将创建者的地址作为参数传入。Vault 将其管理者设置为创建者的地址。此管理者可以调用修改 vault 配置的方法。

此时,创建者应根据需要配置 vault,然后决定 vault 是否要受治理。

  • 如果是,则创建者保留管理者角色或将其转移到另一个地址。
  • 如果不是,则通过将管理者设置为 address(0) 来撤销所有权。此 vault 无法再进行治理更改,并且被认为是最终确定的。

如果需要有限的治理,创建者可以将所有权转移到只能调用治理方法子集的智能合约,可能只有某些参数,或在某些条件下。

即使对于最终将最终确定的 vault,使用治理方法进行 vault 配置也简化了初始化 interface,并且受到 unix 的 fork-exec 分离 的启发。

Vault 将 EVC 身份验证用于管理者,这意味着管理者操作可以批量处理和模拟。但是,Vault 不接受高级 EVC 身份验证方法,如操作员、子帐户和 controlCollateral

使用相同的代码库和工厂,Euler Vault Kit 允许构建托管和非托管借贷产品。托管 vault 旨在长期存在,因此适用于被动存款。如果市场情况发生变化,活跃的管理者可以重新配置 vault 以优化或保护用户。或者,非托管 vault 是静态配置的,用户自己(或更高级别的合约)必须主动监控风险/机会,并在必要时将其存款和头寸转移到新的 vault。

治理风险

可升级/不可变和受治理/最终确定是 vault 的正交属性,可以以任何组合设置。Euler Vault Kit 是一个不可知的 vault 构建系统。由 vault 创建者决定参数和治理结构(如果有),并最终由市场决定哪些 vault 应该获得流动性奖励。

下表指示了 vault 创建者的决策导致的治理风险概况:

<table> <tr> <td></td> <th>可升级</th> <th>不可变</th> </tr> <tr> <th>受治理</th> <td>工厂管理员 + 管理者</td> <td>管理者</td> </tr> <tr> <th>最终确定</th> <td>工厂管理员</td> <td>无</td> </tr> </table>

请注意,创建不可变/最终确定的 vault 也存在风险:

  • 如果市场情况发生变化,使得以前安全的抵押品资产或价格预言机变得不安全,则只能重新配置受治理的 vault。
  • 如果在 Vault Kit 代码中发现关键错误,则工厂管理员只能修复可升级的 vault。

如果发现错误,工厂管理员可能会面临困境。通过修复可升级 vault 中的错误,可能会泄露利用不可升级 vault 的足够信息。工厂管理员应指定明确的安全升级策略,并负责任地评估升级对具体情况的影响。

名称和符号

Vault 具有在创建时确定的固定名称和符号。该符号有三个组成部分:e 前缀、底层资产的符号和一个数字 ID。以下是 USDC vault 的一个可能示例:

  • 符号:eUSDC-1
  • 名称:EVK Vault eUSDC-1

数字 ID 是使用一个简单的 SequenceRegistry 合约分配的,该合约维护不透明字符串指示符的顺序递增计数器。Vault 使用底层符号作为指示符,但任何人都可以随时为任何指示符保留新的 ID。唯一的保证是,对于同一指示符的两个保留将不会返回相同的 ID。

如果底层资产未实现 symbol() 方法,则该符号将被替换为 "UNDEFINED"

具有名称和符号的标准安全属性继续成立。没有任何东西阻止其他人部署一个不相关的 vault,该 vault 也具有符号 eUSDC-1,因此不能假定符号是全局唯一的。

会计

EVault 合约是(大部分情况)符合标准的 ERC-4626 vault,具有额外的功能来实现借款。每个 vault 实例仅持有一种类型的 token:vault 的底层资产(在代码中简称为 asset)。由于 ERC-4626 是 ERC-20 的超集,因此 vault 也是 token,称为 vault 份额或 EToken。这些份额代表对 vault 资产的按比例索取权,并且随着时间的推移,随着利息的累积,可以兑换成更大数量的底层资产。按照 ERC-4626 的建议,份额与其底层资产的 token 具有相同的小数位数。如果底层资产未指定小数位数,则 vault 假定为 18

汇率

Vault 有两类资产,份额可以对其提出索取权:

  • cash:Vault 当前持有的底层 token(以底层资产计价)
  • totalBorrows:未偿还借款,包括应计利息(以底层资产计价)

汇率表示每个 vault 份额值多少底层资产。随着时间的推移,随着利息的累积,汇率将增长。从概念上讲,可以通过将 vault 的总资产(cash + totalBorrows)除以未偿还份额的数量来计算汇率,该数量被跟踪为 totalShares

Vault 合约避免直接将汇率计算为比率,以防止精度损失影响小数位数较少的 token。因此,建议也希望在资产和份额之间进行转换的外部用户使用 ERC-4626 标准中的 convertToAssetsconvertToShares 函数。

首次创建 vault 时,它有 0 个未偿还份额,这意味着上述汇率的简单描述将是不确定的(除以 0)。此外,当份额和底层资产的数量非常少时,舍入的影响更加明显。由于这些原因,vault 对汇率计算应用“虚拟存款”。虚拟存款可以被认为是 1:1 汇率的存款,然后销毁收到的份额(有关更多详细信息,请参阅 OpenZeppelin 的文章)。

结合虚拟存款,完整的汇率方程如下:

exchangeRate = (cash + totalBorrows + VIRTUAL_DEPOSIT)
                  / (totalShares + VIRTUAL_DEPOSIT)

totalBorrows 不一定对应于所有个人债务的总和,因为每个帐户的债务都四舍五入,而 totalBorrows 仅四舍五入一次。

由于虚拟存款份额已纳入汇率,因此它们的价值会随着利息的累积而增加,就像任何其他份额一样。此利息将永久锁定在 vault 中。尽管在大多数情况下,此锁定的利息可以忽略不计,但对于具有高单位价值和/或低小数位数(WBTC、GUSD)的 token 来说,这可能存在问题。为了为预计包含少量存款的此类 token 创建 vault,可以在 token 周围使用 18 位小数的包装器合约。

Token 转移

为了将 token 移入或移出 vault,vault 代码具有内部抽象 pullAssetspushAssets

pullAssets 将首先尝试使用 Permit2 将资产转移到 vault 中。如果此操作不成功,它将尝试使用 vault 的地址作为接收者,在底层资产上调用 transferFrom。Permit2 可以实现更好的用户体验,因为批准可以创建为签名消息,并与 deposit 捆绑在同一个 EVC 批处理中(例如)。尽管用户确实需要首先为 Permit2 合约添加批准,但这只是一次性操作,许多用户在与 Uniswap 或其他应用程序交互时已经完成了此操作。

pushAssets 始终只是在底层资产上使用 transfer。但是,为了防止资金损失,pushAssets 将首先与 EVC 检查以查看接收者地址是否是已知的非所有者地址(虚拟子帐户地址)。如果是这样,它将拒绝转移资产。如果底层资产是 EVC 感知的(可能是嵌套 vault),则可以启用 CFG_EVC_COMPATIBLE_ASSET 配置标志,这将阻止此检查。

如果绕过这些抽象并且底层资产 token 直接转移到 vault,则可以使用 vault 的 skim() 函数恢复这些 token。第一个调用 skim() 的用户将通过执行隐式存款来声明它们。在某些情况下,这可以用作更节省 gas 的存款方法,但是必须注意不要让其他人在中间 skim token。

内部余额跟踪

为了检索它当前拥有的底层资产的数量 (cash),类似 vault 的合约有两种选择:

  • 通过调用 underlying.balanceOf(address(this)) 从底层资产读取自己的余额
  • 在存储中保留预期值的内部副本,并在 token 移入或移出时更新它

Euler Vault Kit 使用第二种方法,称为内部余额跟踪。内部跟踪余额可防止用户通过将底层资产直接转移到 vault 来操纵汇率,这在尝试定价 vault 的份额时可能很危险。内部余额跟踪也为许多常见操作使用稍微更少的 gas。

另一方面,不支持在显式转移之外更改余额的 token,例如 rebasing/fee-on-transfer token,因为 vault 不知道意外的余额变化。这不被视为一个重要问题,因为借出此类 token 有几个 众所周知的问题,并且始终可以在它们周围构建规则化的包装器合约。

舍入

在内部,相对于负债 token 的小数位数,债务以额外的精度跟踪。在外部,债务金额始终向上舍入到负债 token 中可表达的下一个最小增量。这确保借款人始终偿还至少他们借入的金额以及任何应计利息。

可以为给定数量的份额赎回的资产数量始终向下舍入,并且提取给定数量的资产所需的份额数量向上舍入。这确保存款人不能提取超过他们存入的金额加上赚取的利息。

这两种行为确保舍入的所有影响都有利于 vault(即,剩余的存款人)。但是,由于舍入代表对 vault 的 隐式捐赠,这意味着它会影响汇率。这是虚拟存款的原因之一:即使使用空 vault,舍入对汇率的影响也是微不足道的。

在数量舍入到正好 0 的退化情况下,该操作被视为无操作(不发生任何事情)。

DToken

虽然债务与余额一起在 vault 存储中跟踪,并且可以使用 debtOf() 读取,但 vault 也提供 DToken 作为债务的只读 ERC-20 interface。每当债务金额发生变化时,vault 会调用 DToken 合约以触发 Transfer 日志。请注意,DToken 日志是债务变化的净额(应计利息加上/减去借款/偿还金额)。

DToken 的主要目的是用于链下分析:债务修改在区块浏览器中清晰显示,并且税收会计软件可以跟踪借款/偿还事件。由于合约不支持转移或批准,因此希望债务可移植性的高级用户应使用 vault 合约上的 pullDebt() 函数。这允许任何人自愿承担任何其他人的债务,前提是控制器 vault 允许这样做(通常只要拉取者已启用控制器并拥有足够的抵押品)。

DToken 合约是 EVault 创建的第一个(也是唯一)合约,因此它的地址可以从 vault 的地址和nonce 1 计算得出。此方法可能不适用于所有 EVM 链。

余额转发

除了纯利息之外的额外激励措施可能是流动性的重要驱动因素。这些奖励激励措施可以构建到 vault 合约本身(即 Compound)中,也可以完全在链下分配(即 Euler V1)。将奖励逻辑直接集成到 vault 合约本身中可以实现即时和无需信任的分配,但可能会增加每个人的 gas 成本(即使他们的奖励微不足道),更重要的是,可能需要在 vault 部署时决定静态奖励发行策略。

余额转发 是一种尝试获得两全其美的方法。如果一个帐户选择加入,则每次其余额发生变化时,都会通知外部合约更新的余额。此外部合约在 vault 中没有特殊权限,但是应注意确保其 balanceTrackerHook 方法不会恢复或耗尽所有 gas,因为这可能会导致关键的 vault 操作失败。

名为Reward Streams的通用系统是余额转发合约的推荐实现。它允许任何人随时以任何 token 无需许可地激励任何 vault。

利息

复合

利息在区块中的第一笔交易中累积:必须经过实际时间才能累积任何利息。但是,每当余额或债务金额发生变化时,就会确定新的利率目标。这是在 checkVaultStatus 函数中完成的,以便多次与 vault 交互的批处理只需要重新确定一次利率目标。

Vault 每秒确定性地使用指数计算复利。由于应计利息会添加到 totalBorrows 中,因此它会增加利用率(借出的 vault 资产的比例)。除此之外(以及累加器舍入的影响),应付/赚取的利息金额与合约交互的频率无关。

利率模型

利率模型 (IRM) 是根据 vault 的状态确定应收取的利率的合约。通常它们是基于利用率的纯函数,但这不是必需的。如今,最常见的函数是“线性弯曲”模型,该模型以逐渐倾斜开始,然后在特定的目标利用率值处突然变得陡峭。IIRM interface 的 computeInterestRate 函数接受计算利用率所需的最低限度。

Vault 在 vault 状态检查期间调用它们的 IRM。这意味着如果在 EVC 批处理中与 vault 交互多次,则 IRM 只需要调用一次,在批处理结束时。Vault 将此利率缓存在它们的存储中,以便在后续区块的第一次操作时可以累积利息。利率必须预先计算并缓存,以便非纯 IRM 将无法追溯更改观察到的利息累积。可以随时使用 touch 方法调用非纯 IRM 以重新确定利率目标(存款人在重新确定向上目标时触摸,借款人则相反)。

IRM 可以查询 vault 以获取其他信息,而无需触发只读重入保护,因为此锁定在 vault 状态检查时释放。可能有用的示例是嵌套 vault,它们可能部分地从其父 vault 的状态派生其利率,或者实现合成资产的 vault。

IRM 返回借款人必须支付的利率。存款人(也称为供应商)通常会收到比此利率低的利率,因为收到的利息会在所有存款人和 interestFee 之间按比例分配。

IRM interface 以“每秒百分比收益”(SPY) 值指定利率,这些值是按秒计算的复利,按 1e27 缩放。尽管合约本身未使用,但为了一致性,我们建议将 SPY 转换为年化等效值(在 UI 和其他地方)应使用平均格里高利历年 365.2425 天中的秒数。

当 vault 安装了 address(0) 作为 IRM 时,假定利率为 0%。如果对 vault 的 IRM 的调用失败,则 vault 将忽略此失败并继续使用之前的利率。如果在安装后立即 IRM 失败,则将改为使用 0%。在 IRM 调用中耗尽 gas 但否则有足够的 gas 成功完成的操作可能会延迟更新利率,但这将在下次与 vault 交互时更正。

尽管大多数 IRM 实现纯函数,但在重新确定利率目标时,vault 不会使用 staticcall 调用它们,以支持有状态或反应性 IRM。computeInterestRate() 方法应验证 msg.sender == vault。IRM 还必须实现相应的 computeInterestRateView() 方法,该方法不更新状态。即使 vault 将当前利率存储在其存储中,computeInterestRateView() 也可用于获取批处理中间的更新利率(尤其是在模拟期间),因为存储的利率可能已过时。

费用

每当累积利息时,一部分利息会分配给费用。这类似于其他协议中储备金的概念。分配给费用的利息比例由管理者作为参数 interestFee 控制,尽管这由 ProtocolConfigisValidInterestFee() 方法验证。

每当累积利息时,每个借款人的负债都会相应增加。利息费用的收取方式是创建必要数量的份额,以使存款人的利息稀释 interestFee 的一部分(有关推导,请参阅 Euler V1 白皮书)。由于费用以 vault 份额而不是底层资产计价,因此未提取的费用本身会随着时间的推移赚取复利。

目前,在计算为费用创建的份额数量时,会使用新的汇率,并且忽略虚拟存款。这通常会使费用略低于它们原来的费用,尽管实际上应该可以忽略不计。

费用分成

为了提高效率,应计利息费用在 vault 内的一个特殊的虚拟帐户中内部跟踪。为了将这些费用转换为可以兑换为底层资产的普通份额,可以在 vault 上调用 convertFees 方法。任何人都可以随时调用此方法。

convertFees 计算自上次调用以来累积的新份额数量,然后调用 ProtocolConfigprotocolFeeConfig() 方法。这将返回费用分成,费用分成是应付给 Euler DAO 及其接收地址的利息费用的比例。Vault 验证费用分成,然后将此比例的应计利息转移到指示的地址。剩余部分转移到 vault 的管理者指定的 feeReceiver 地址。如果未指定 feeReceiver,则此部分也转移到 Euler DAO 的接收者。

请注意,由于 ProtocolConfig 可以随时更改费用分成,因此发送给 Euler DAO 的应计利息部分直到调用 convertFees 才最终确定。如果 vault 管理者担心这一点,则应经常调用 convertFees

ProtocolConfig

ProtocolConfig 合约是 Euler DAO 在 vault 套件生态系统中的利益代表。Vault 允许此合约控制的内容受到严格限制。对于不可升级的 vault,这些限制将永久执行。

虽然 interestFee 等参数协商存款人和 vault 管理者之间的关系,但 ProtocolConfig 协商 vault 管理者和 Euler DAO 之间的关系。

ProtocolConfig 公开以下由 vault 调用的方法:

  • isValidInterestFee():确定是否允许利息费用的值。只有当管理者尝试将利息费用设置在 10% 到 100% 的范围之外时,Vault 才会调用此方法(保证范围)。
  • feeConfig():在转换费用时调用此方法。它返回以下内容:
    • feeReceiver:DAO 的费用份额的接收地址。
    • protocolFeeShare:应发送给 DAO 的利息费用的比例。如果 ProtocolConfig 发回的值大于 50%,则 vault 将忽略此值并改为使用 50%。

风险管理

为了使 EVC 能够检测到不允许的用户操作,EVC 要求 vault 实现以下两种方法:

  • checkAccountStatus:指定的帐户是否违规?如果一个帐户有任何负债,其抵押品的价值是否足够?
  • checkVaultStatus:vault 本身是否健康?是否已超过借款和供应上限等 vault 级别限制?

EVC 在适当的时间调用这些方法。通常这会在批处理中的所有操作都已执行之后进行,因为 EVC 允许用户推迟这些检查,作为一种闪电流动性。如果 vault 想要指示状态检查失败,它会恢复,中止交易和所有执行的操作。

checkAccountStatus 是一个 view 方法(用 STATICCALL 调用),以防止恶意用户在 vault 份额的常规 transfer 操作上执行代码,从而可能保护未正确实现重入锁的第三方合约。另一方面,checkVaultStatus 从未在此上下文中调用,因此可以使用常规 CALL 调用它。EVK 利用这一点来记录 vault 状态的初始快照。

LTV

为了使借款发生,负债 vault 的管理者必须决定一组抵押品资产及其允许的最大贷款价值比率(从这里开始简称为 LTV)。应仔细选择 LTV。除了底层资产中固有的风险之外,还应考虑包含该资产的 vault。如果抵押品本身可以借出以换取有风险的抵押品或使用不安全的价格预言机,则可能需要较低的 LTV。

为了添加抵押品并配置其 LTV,管理者调用 setLTV() 方法。指定的 LTV 是 0 到 1 之间的分数(按 10,000 缩放),并且对应于在确定帐户是否违反其 LTV 时应用于抵押品价值的风险调整。虽然 EVC 确保每个帐户最多只有一个负债,但贷款可以由多个抵押品支持。

风险调整

为了确保有足够的抵押品可用于在必要时支付清算折扣,每个帐户的抵押品价值必须大于其负债,并具有一定的安全缓冲。风险调整过程用于确定此安全缓冲的大小。

为了计算风险调整后的抵押品价值,vault 将所有抵押品余额转换为一个通用货币,将每个抵押品余额乘以其相应的 LTV,然后将它们加起来。由于 LTV 始终小于 1,因此风险调整始终会降低抵押品价值。

为了使帐户健康,其风险调整后的抵押品价值必须严格大于其负债价值,或者负债金额本身必须正好为零。当帐户不健康时,它被认为是违规

在确定帐户是否健康时,负债 vault 将迭代帐户的抵押品,并保持风险调整后价值的滚动总和。如果此总和超过负债的价值,则它将停止迭代:已知该帐户是健康的。如果有使用昂贵的价格预言机,这可以节省大量 gas。用户可以使用 EVC 的 reorderCollaterals 函数优化其抵押品条目的顺序。此实现的灵感来自 Gearbox。如果管理者尚未为抵押品设置 LTV(或 LTV 为 0),则这将不会对帐户的风险调整后的抵押品价值产生任何影响,并且 vault 将不会浪费Gas来尝试对其进行定价。

借款 LTV vs 清算 LTV

setLTV() 接受单独的借款和清算 LTV 值。借款 LTV 必须小于或等于清算 LTV。对于这两种类型的 LTV,风险调整过程都是相同的。上下文确定使用哪一个:

  • 借款 LTV 在帐户状态检查中使用,其效果是限制借款的规模。如果一个帐户在这里违规,这意味着它无法执行任何操作,除非可以改善其健康状况(存款和偿还),除非这些操作解决了违规问题。
  • 清算 LTV 在尝试清算帐户时使用。只允许清算违规帐户。这意味着清算 LTV 具有限制现有借款的效果。

拥有两个 LTV 的主要目的是补偿定价延迟和不确定性。假设攻击者知道即将发生非常大的价格变动,要么是因为价格提要延迟,要么是因为他们正在操纵预言机。攻击者可以存入一些抵押品,尽可能多地借款,然后等待价格更新。如果价格更新足以将帐户推过 LTV 安全缓冲,则帐户的负债将成为坏帐(负债价值超过抵押品价值)。然后,攻击者可以清算自己以收回所有抵押品,从而使 vault 的存款人遭受损失。下面我们描述了这种攻击的另一个变体

在借款 LTV 和清算 LTV 之间添加差距会增加利用的难度,从而要求攻击者找到或导致成比例地更大的价格变动。尽管它可以是一个有用的风险管理工具,但它不能解决根本问题,并且需要权衡:从概念上讲,当向 vault 添加差距时,可以提高清算 LTV,从而通过延迟清算来增加 vault 的风险,或者可以降低借款 LTV,从而降低借款人的可用杠杆并降低 vault 的吸引力。 另一种有效增加差距的方法是使用具有非零买卖价差的价格预言机。这使得对定价来源准确性具有最多上下文信息的组件能够将实时定价风险传递给 vault,然后 vault 可以将此信息用作动态借贷 LTV

不受信任的抵押品

选择合适的 vault 作为允许的抵押品对于 vault 的安全性至关重要。所有 vault 必须与 EVC 兼容,并且必须使用相同的 EVC 部署。该 vault 还必须能够通过配置的价格预言机进行定价。

使用具有流动性不足或可操纵的基础资产的 vault 可能会威胁到存款人的安全。并且具有危险配置的 vault(例如极高的 LTV 或自身选择了糟糕的抵押品)可能会遇到坏账的情况。

评估实施每个抵押 vault 的智能合约代码也至关重要。编码错误或恶意的 vault 可能会拒绝在清算事件中释放资金,或者只是谎报其持有的价值。因此,建议仅使用已知良好工厂创建的 vault。可以通过调用工厂的 isProxy() 函数来验证 vault 是否是由工厂创建的。

使用安装了 hooks 的抵押 vault 时也应谨慎。Hooks 可以实施条件检查,这些检查(恶意与否)可能会阻止清算成功。

在评估新的或自定义的 vault 实施是否值得信赖时,应执行所有通常的检查,例如验证代码是否经过审计,是否不包含后门等。此外,验证 transfer 方法是否调用任何可能运行攻击者代码的外部合约非常重要。这是因为 EVK 实施在清算后豁免账户的状态检查,恶意用户可能会执行意外获得豁免的操作。

因此,清算的一个重要属性是没有 LTV 的资产永远不会被清算人没收(用户可以在其 EVC 抵押品集中安装任何 vault,无论是否受信任)。抵押资产的 LTV 可以降低回 0,但这不会消除清算它们的能力,因此这不能用于恢复已安装为抵押品但随后发现不安全的 vault(因为其 transfer 方法中存在错误)。

非抵押品存款

尽管只有明确配置了 LTV 的 vault 才能用作支持债务的抵押品,但当账户不健康时,账户的功能将受到限制。这包括非抵押资产的提款失败。每个账户都被视为一个单独的头寸,当头寸不健康时,控制器 vault 被认为有权通过任何方式激励用户偿还债务。要完全隔离资产,即使它们不用作抵押品,也请使用不同的子账户来存储存款。

但是,即使在用户违规且无法访问非抵押品存款时,EVC 仍然阻止控制器 vault 实际扣押这些资产进行清算。即使在技术上可行的情况下,EVK 也不试图扣押违规者在责任 vault 本身中的份额(考虑到没有理由在同一 vault 中同时持有份额和债务)。

LTV 渐变

一项或多项抵押资产的 LTV 可以由管理者修改。如果 LTV 突然降低,任何未偿还的借款人可能会立即陷入违规状态。由于反向荷兰式拍卖清算系统,这些借款人可能会因这一行动而不公平地损失大量价值。

一种解决方案可能是为现有借款保持较高的清算 LTV,但降低新借款的 LTV。这将更加公平,但会产生不希望的影响,即将这些高 LTV 的借款无限期地保留在 vault 的账簿上。

相反,管理者可以在更改清算 LTV 时指定一个 ramp duration(渐变持续时间)。渐变仅影响清算 LTV——新的借款将立即需要使用新值。渐变导致清算 LTV 在指定的持续时间内线性下降到新值。在该窗口中的某个时刻,受影响的头寸将变得不健康,但只是稍微不健康。此时或之后不久,这些头寸将以对借款人最小损失进行清算。

如果在另一个渐变正在进行时启动一个渐变,则先前渐变上的当前位置将成为新渐变的起点,以防止突然跳跃(仅影响斜率)。

即使最终确定的 vault 也可以从 LTV 渐变中受益,方法是安装一个有限的管理者合约,该合约可以在某些条件下启动平稳的结束流程。

供应和借款上限

管理者可以配置供应上限、借款上限或两者兼有。供应上限是对可以存入 vault 的基础资产数量的限制,而借款上限是对可以借入的数量的限制。两者都以基础资产计价,但被打包成 2 字节的十进制浮点值。

上限可以暂时违反,因为它们仅在批处理结束时强制执行。如果在批处理开始时未违反上限,但在结束时违反了上限,则交易将被恢复。

即使它不能是用户操作的直接结果,在某些情况下,上限可能会持续违反。例如,如果管理者降低了上限,或者应计利息导致总借款超过借款上限。如果由于这些原因之一,上限在批处理开始时被违反,则只有在上限违法行为有所减轻(或至少没有变得更糟)的情况下,交易才会成功。

请注意,原则上,这种行为可以通过机会主义地包装 gasless transactions(无 gas 交易)来加以利用,这些交易将 withdraw(提取)/repay(偿还)到周围的批处理中,从而存入/借入等量的金额。执行者实际上能够将用户的供应/借款配额转移到他们自己的账户中,而不是减少上限值。

在某些非常不寻常的情况下,审计人员发现,由于四舍五入,当 vault 的供应上限超过时,repay() 操作可能会失败。在这种情况下,用户应确保他们不在同一批处理中偿还多个子账户,并且/或者将除了一些 dust(灰尘)之外的所有债务提取到另一个子账户中,然后从那里偿还。由于这些情况非常罕见(我们预计它们永远不会在现实世界中出现),因此认为没有必要在合约中阻止这种情况。

Hooks

Vault 支持有限的 hooking 功能。为了使用它,管理者应该安装一个 hook 配置,该配置由两个参数组成:

  • Hook 目标:合约的地址或 address(0)
  • Hooked ops(Hook 操作):一个位域,用于指定受 hooks 影响的操作

大多数用户可调用的外部函数都被分配了常量。例如,存款函数具有 OP_DEPOSIT。hooked ops 位域是这些常量的按位或。

当函数被调用时,vault 检查相应的操作是否在 hooked ops 中设置。如果是,则使用提供给 vault 的相同 msg.data along with(以及)附加的 EVC 认证的调用者作为尾随调用数据来 call(调用)hook 目标。如果调用 hook 目标失败,则 vault 操作也会失败。如果 hook 目标是 address(0)(或任何非合约地址),则操作将无条件失败。

当首次创建 vault 时,所有 hooks 都已启用,hook 目标是 address(0)。在这种状态下尝试与 vault 交互将抛出 E_OperationDisabled 错误。为了使 vault 正常运行,应禁用所需的 hooks,例如,通过调用 setHookConfig(address(0), 0) 来启用所有操作。Vault 在此禁用状态下启动是为了避免竞争条件,在这种竞争条件下,用户可能会开始与部分配置的 vault 交互,例如在配置供应上限之前进行存款。

除了用户可调用的函数之外,hooks 还可以 hook checkVaultStatus。当 EVC 在 vault 上调用 checkVaultStatus 时,将调用它,这通常是在与 vault 交互的任何批处理结束时。此 hook 可用于拒绝违反 vault 的“后置条件”属性的操作。

已安装的 hook 目标合约地址可以绕过 vault 的只读重入保护。这意味着 hook 函数可以调用 vault 上的 view 方法。但是,由于正常的重入锁定,hooks 无法执行状态更改操作。

某些 hook 配置可能导致 vault 不能完全 ERC-4626 兼容

Hook 用例

Hook 系统的主要目的是禁用 vault 功能,无论是永久性的还是在某些条件下。Vault 管理者不能使用 hooks 来破坏 vault 中的核心会计不变性。

  • 暂停守护者:管理者可以设置为充当 pause guardian(暂停守护者)的合约。此合约可以允许某些受信任的用户暂停和取消暂停各个 vault 操作,包括存款、提款、借款、偿还、转账和清算。这可能会安全地结束陷入困境的 vault、防止黑客攻击或在黑客攻击后安全地恢复资金。如果 vault 管理者想要确保不允许某些暂停操作组合(例如,在禁用偿还时阻止清算),则暂停守护者合约应强制执行此操作。
  • 合成资产 Vaults:这些是 specialised vaults(专用 vault),需要 hooks 提供一些限制。特别是,只允许基础资产本身存入其中,并且某些其他操作被完全禁用。
  • 许可/RWA Vaults:某些 vault 创建者可能希望限制谁可以从 vault 存款和/或借款,无论是出于合规性原因还是因为借款的抵押不足。
  • 闪电贷费用:通常,flashLoan 函数不收取任何费用。但是,hook 可以强制在允许贷款继续之前,将闪电贷的一定百分比作为费用支付。
  • 利用率上限:hook 可以通过在 checkVaultStatus 中恢复来防止 vault 的利用率超过特定水平。请注意,根据具体要求,hook 目标可能需要实施“快照”,其中 vault 的初始状态在操作启动时记录在(可能是临时的)存储中,然后在 checkVaultStatus 中读取和清除。
  • 最小债务规模:为了防止用户创建过小的 dust 头寸而无法盈利地清算,可以为每个更改债务的操作安装 hook。这些 hooks 会在瞬态 Set 中记录账户,然后验证每个账户的 debtOf() 是否高于 checkVaultStatus hook 中的阈值。
  • 限制抵押品数量:EVC 将账户可以同时启用的抵押资产数量限制为 10。这对于维持清算成本的合理上限非常重要。但是,可能因为他们配置了特别昂贵的预言机,vault 可能会选择将其限制为更小的数字。

价格预言机

定价 Shares

在 vault 内部,每个抵押品都配置为另一个 vault 的地址,而不是基础资产(除非资产经过特殊构造也可以用作抵押 vault)。这意味着用户抵押品的价值实际上是 vault 的 shares 的价值。由于 汇率,vault share 不一定等于一个单位的基础资产。

因为将 shares 的数量转换为基础资产金额本身就是一个定价操作,所以这个责任被委托给价格预言机。在某些跨链设计中,价格预言机还可能负责确定单独链上相应 vault 的汇率。

对于使用 Euler Vault Kit 创建的 vault,可以使用 ERC-4626 convertToAssets 函数来以基础资产单位定价 shares。此函数旨在成为可靠的预言机。内部余额跟踪可防止直接转账捐赠的操纵,并且 虚拟存款 最大限度地减少了基于四舍五入的“隐形存款”的影响。有关这些保护措施的更多详细信息,请参阅我们的文章。作为额外的防御层,建议 vaults 始终存入少量资金,因为大多数假设的基于四舍五入的攻击向量都依赖于空或接近空的 vaults。

IPriceOracle

每个 vault 都安装了价格预言机的地址。此地址是不可变的,即使是 vault 管理者也无法更改。如果需要更新定价来源,则此地址应该是受管理的 EulerRouter 定价组件。所有预言机都必须实现 IPriceOracle 接口,特别是以下两个函数:

/// @notice One-sided price: How much quote token you would get for inAmount of base token, assuming no price spread
/// @notice 单边价格:假设没有价格差,你将以 inAmount 的基础代币获得多少报价代币
function getQuote(uint inAmount, address base, address quote) external view returns (uint outAmount);
/// @notice Two-sided price: How much quote token you would get/spend for selling/buying inAmount of base token
/// @notice 双边价格:对于出售/购买 inAmount 的基础代币,你将获得/花费多少报价代币
function getQuotes(uint inAmount, address base, address quote) external view returns (uint bidOutAmount, uint askOutAmount);

这些方法不公开任何定价级别的配置。相反,对于自定义定价配置,必须部署新的预言机合约。

报价

价格分数永远不会由接口直接返回。相反,预言机的行为就像它在报价 swap 金额一样。这避免了某些灾难性的精度损失,尤其是在使用低小数代币的情况下(请参阅 SHIB/USDC 的示例)。

价格预言机可能会合法地返回 0,以表明所请求的报价金额毫无价值,要么是因为价格非常低,要么是因为报价金额非常小,或者两者兼而有之。合法的定价错误通过预言机恢复来发出信号。这反过来会导致 vault 恢复,这是危险的,因为它会破坏 vault 的功能(最重要的是,清算)。因此,应仅使用可靠的价格预言机。

getQuote 函数返回在当前边际(无价格影响)价格下,inAmountbase 代币将购买多少 quote 代币。getQuotes 方法(复数)可以返回指定数量 base 的买入和卖出 quote 金额。尽管简单的预言机会为买入和卖出返回相同的金额,但更高级的 vault 可能会使用它来公开当前的市场价差。或者,可以表达置信区间,可能会通过查询多个预言机、对它们进行排序并将最低金额作为买入价和最高金额作为卖出价返回。

Vault 使用买入价和卖出价之间的差异作为市场深度或不确定性的代理。当验证新的贷款是否可接受时,使用抵押品的买入价和负债的卖出价计算 LTV。这可以被认为是相对于买卖价差宽度的借款 LTV 的动态调整。另一方面,当清算现有贷款时,使用 getQuote 中点价格,因此贷款不会因暂时广泛的价格价差而清算。Vault 假定价格预言机尊重以下不变量:bid &lt;= mid-point &lt;= ask

因为接口同时接受 basequote 地址,所以可以将预言机配置为计算组合多个定价来源的 cross(交叉)价格。这在概念上类似于 swap 有时通过一系列 DEX 池进行路由的方式。至少,预言机应该能够将 shares 交叉定价到资产汇率与基础资产的价格。

Vault 配置

虽然每个 vault 在创建时都指定了一个预言机地址,但预言机不需要为其所有定价使用同类来源。例如,具有 USDC 作为基础资产的 vault 允许将 DAI、USDT、WETH 和 UNI vault 用作抵押品,可以选择对其 DAI/USDC 价格使用 1:1 Hook,对 USDT/USDC 使用 Chainlink,对 WETH/USDC 使用 Uniswap3 TWAP,并将其 UNI 的定价转发给另一个 IPriceOracle 部署。由于 shares 定价,即使两个具有相同基础资产的 vault 也可以使用不同的预言机。

请注意,定价预言机配置始终是本地的,对于责任 vault 而言,抵押 vault 不知道或不关心此配置。如果我们的示例中的 DAI vault 允许针对 USDC 抵押品进行借款,则它可以为其自己的定价使用完全不同的预言机。这类似于传统金融,在传统金融中,贷方有责任充分评估抵押品价值以确保贷款安全。由于各种 vault 可以使用不同的定价来评估相同的抵押 vault,因此无需按预言机类型拆分流动性。

记账单位

Vault 还指定 unit of account(记账单位)参数(有时称为“参考资产”)。此参数是不可变的,即使是 vault 管理者也无法更改。记账单位作为所有查询中的 quote 参数传递给价格预言机,以便所有抵押品和负债都以通用资产定价。如果记账单位与 vault 的基础资产相同,则需要的价格转换会减少一个。对于某些风险配置,这也可能会提高价格质量:考虑使用 DAI 借用 USDC,或使用 ETH 借用 stETH。在这些情况下,通过不相关的中间资产定价可能会导致不必要的影响。通过直接定价,可以避免虚假清算,并且可以配置更高的 LTV 比率。

清算

当账户的风险调整后抵押品价值达到或低于其负债价值时,该账户有资格进行清算。寻求利润的清算机器人会离线监控账户,并在满足此条件后尝试清算它们。

在清算中,估值是使用 mid-point(中点)价格完成的,风险调整是使用 liquidation LTVs(清算 LTV)完成的。

折扣

在清算期间,清算人将从他们选择的账户抵押 vault 中收到 shares,并承担来自责任 vault 的债务。为了激励清算人,收到的抵押 shares 将比收到的债务更有价值。或者,换句话说,清算人以 discount(折扣)购买抵押品。

清算逻辑源自 Euler V1 的反向荷兰式拍卖系统。这会根据头寸的违规程度按比例缩放折扣。如果违规者只是稍微违规,则清算可能没有足够的盈利能力来克服滑点、gas 费等。但是,随着价格对违规者不利,折扣将会增加,并且在某个水平上,机器人会检测到清算略有盈利并且会执行它。尤其是对于平滑价格(例如 TWAP),甚至对于离散更新的馈送(例如 Chainlink),这已被证明是一种成功的系统,可以在不过度惩罚借款人或奖励 MEV 机器人的情况下保护协议的安全。

因为它们导致了费用拍卖的中断,并且可能在 vault 运营商和借款人之间产生不良激励,所以在清算时累积到储备金的 Euler V1 费用已被删除。由于存款人的损失现在从 0% 开始,因此对于杠杆头寸的用户来说,清算的破坏性较小。因此,为了简化代码,违规账户的完整头寸有资格进行清算。

清算折扣拍卖也与 LTV 渐变 相辅相成,允许托管 vault 的管理者修改风险配置,而不会对受影响的借款人造成过多的处罚。

最大折扣

在清算中,允许的折扣被限制为 maximum liquidation discount(最大清算折扣)参数。Vault 创建者 必须 在创建其 vault 后将其设置为适当的值,否则 0 的默认限制将阻止授予任何折扣,并且没有人会受到激励来执行清算。

在选择最大折扣时,应考虑 OpenZeppelin 的 2019 Compound audit,“适得其反的激励”部分中的一个观察结果:

任何以折扣抵押品激励清算人的清算系统在某些情况下都会使违规者比清算前 不健康。在 EVK 中,折扣与用户的不健康程度成正比,这意味着在这些情况下,清算人可以通过执行许多小清算来提高其总收益,而不是一次大规模清算。每次较小的清算都会降低用户的健康状况,从而增加他们后续清算的折扣,直到达到最大清算折扣(与始终处于“最大”的 Compound 等系统相反)。正如我们的 荷兰清算分析 研究论文中所述,可以通过选择适当低的“最大”折扣因子来避免这种情况。

冷静期

虽然从根本上来说,始终可以使用价格操纵来攻击借贷市场,但一些预言机面临特殊的挑战。特别是,像 Pyth 和 RedStone 这样的基于拉取的预言机为攻击者提供了更大的灵活性,因为他们通常可以选择使用 N 分钟窗口内的任何已发布价格。例如,攻击者可能会离线监控价格,等待 vault 抵押资产价格大幅下跌(或者等效地,责任资产价格大幅上涨)。如果跌幅足够大,攻击者将搜索先前 N 分钟的价格,并选择差异最大的一对。然后,攻击者将提交一个执行以下攻击的交易:

  • 使用旧价格更新预言机
  • 存入抵押品并尽可能多地借款
  • 使用新价格更新预言机,导致头寸变得非常不健康
  • 从另一个单独的账户清算该头寸,留下坏账。此坏账对应于通过攻击获得的利润,但以 vault 存款人的利益为代价

为了应对这种攻击,管理者可以配置“冷静期”,这是账户无法被清算的一段时间。冷静期在账户成功通过账户状态检查后开始,并持续管理者可配置的秒数。通过设置非零冷静期,账户无法在他们之前健康的区块内被清算。请注意,违规账户无法在不恢复健康的情况下延长其冷静期。

这样做的结果是,上述攻击无法再以完全无风险的方式进行。头寸需要在一个区块中设置,但在随后的一个区块中清算,这可能会为其他无关方打开机会,让他们代替攻击者执行清算。此外,此类攻击无法通过闪电贷融资。除了与价格预言机相关的攻击之外,此保护还可以减少未来未知协议攻击的影响。

更一般地说,冷静期允许 vault 创建者表达特定链的最低预期活跃期。如果可以估计最大可能的审查时间,则可以将冷静期配置为大于此值,但权衡是,新头寸的合法清算可能会在此期间延迟。

坏账社会化

Vault 的管理者可以启用 bad debt socialisation(坏账社会化)。如果启用,当违规账户的所有抵押品在清算期间被没收但仍有非零负债时,此负债被称为 bad debt(坏账)并被取消,将损失社会化给 vault 中的所有当前存款人。机会主义的存款人原则上可以在社会化之前立即提取以避免这种损失,但这可能会被避免 所有 存款人最终尝试提取的银行挤兑情况的长期利益所抵消。此实现受到 Morpho Blue 的启发。

不希望进行债务社会化的 Vault 管理员可以使用 setConfigFlags(CFG_DONT_SOCIALIZE_DEBT) 禁用此功能。如果是这样,应该设计一种处理坏账情况的替代方法(即,保留一些费用份额作为储备金,或者保留一个通过 pullDebt() 承担债务的慈善方)。

因为 账户的所有 抵押资产必须具有完全为 0 的余额,所以 dust 余额可能会阻止债务社会化。但是,通常至少有一方有动力保持资金池的健康,以保护自己的资金(无法一次性提取所有资金的大额存款人)或维护费用收入(vault 管理者/Euler DAO),因此他们可能会选择花费 gas 来清算 dust。可以通过发布在账户的所有抵押品上循环并清算每个抵押品的交易来处理不断抢先 dust 存款的活跃用户。这所需的 gas 是合理的,因为许多存储访问都被摊销了。

为了允许链下用户使用事件日志准确跟踪总借款和内部余额,坏账社会化会发出 RepayWithdraw 日志,其中 repay 似乎来自清算人,而 withdraw 似乎来自 address(0)。这意味着在链下跟踪余额时,address(0) 的余额可能会变为负数。

债务社会化的一个结果是,如果抵押 vault 上的社会化充分贬值了抵押 shares,那么 vault 上的头寸可能会在基础资产上没有价格变动的情况下陷入违规。如果 vaults 相互配置了 LTV,则可能会出现清算螺旋。

替代清算

虽然 vault 的清算系统对于保护系统是必要的,但用户可以选择使用 EVC 运营商 选择替代的账户保护。

本质上,清算相当于止损单。只要你在进入违规状态之前将止损阈值设置为触发,你就可以按照自己的条件平仓。以下是用户可以选择使用替代清算机制进行自定义的一些事项:

  • 清算人/执行者的不同奖励结构:
    • 固定折扣,按 gas 成本缩放,使用替代代币支付等
  • 比简单的净资产价值更具体的触发条件
  • 显式滑点限制或强制交换场所
  • 替代价格预言机

视角

本节仍在开发中,如有更改,恕不另行通知

由于 EVK 是一个 工具包,因此它会尽最大努力保持灵活性,并且不会对 vault 创建者强制执行策略决策。这意味着可以创建具有不安全或恶意配置的 vaults。此外,原本安全的 vault 可能因为接受 insecure collateral as collateral(不安全的抵押品作为抵押品)(或者抵押 vault 本身接受不安全的抵押品,等等,递归地)而变得不安全。

视角提供了一种使用链上可验证逻辑验证 vault 属性的机制。视角是任何实现以下接口的合约:

interface IPerspective {
    function perspectiveVerify(address vault, bool failEarly) external;

    function isVerified(address vault) external view returns (bool);
}

perspectiveVerify() 将检查提供的 vault 的配置,并确定它是否满足此特定视角的所需属性,如果满足,则将其记录在其存储中。此记录的事实可以被认为是缓存或记忆的值,因此 gas 昂贵的验证只需要发生一次。之后,可以使用 isVerified() 廉价地读取此缓存的结果。

请注意,不一定有任何机制来使缓存无效,因此大多数视角应拒绝安装了管理者的 vault,因为管理者可能会将配置更改为不合适的配置。或者,视角可能会检查管理者是否是受信任的实体,或者管理者是否是有限的合约,并且验证的值可以由管理者更改。

代币列表

视角的主要用例是以无许可的方式,链上近似 Token Lists(代币列表)。视角不会取代代币列表,并且用户界面可以选择同时使用这两种系统。一个 vault 可以与许多视角匹配,并且没有任何人阻止任何人出于任何目的创建新的视角。

就像代币列表一样,用户界面将允许用户导入他们想要用于过滤不符合其信任标准的 vaults 的视角。高级 UI 可能支持特殊的过滤功能,例如“满足 2 个或更多配置视角的‘所有’vaults”,或“此视角上的‘所有’vaults,但‘不’在此视角上”。

虽然任何符合 IPerspective 的合约都可以用作视角,但我们创建了一个 flexible reference implementation(灵活的参考实现),用户或项目可以根据自己的要求进行调整。

自定义白名单视角

最简单的视角形式是白名单。此实现是一个不受监管的、不可变的视角,它具有在创建时提供的静态 vault 列表。

拥有一组不通过默认透视的自定义 vaults 的不可变项目可能需要用户导入自定义透视才能使用其 vaults。与代币列表和其他无许可方法一样,用户必须注意不要导入恶意的钓鱼透视。

受监管的视角

这是一种白名单视角,它有一个活跃的管理者,可以根据他们想要的任何标准添加或删除 vaults。

这些视角旨在由受信任的各方运营,这些各方被赋予了管理 vaults 的责任,例如必须显示默认 vault 列表的 UI。

托管抵押品视角

另一个简单的视角称为“托管抵押品”。这些视角验证 vaults 是否配置为不允许借款,并且是不可变的且已最终确定。由于此类 vaults 不会产生收益,因此它们仅用于存储代币以用作其他 vaults 中的抵押品。

托管 vaults 不能配置任何抵押品,因此托管抵押品视角永远不需要递归到其他 vaults 中。

不受监管的视角

最复杂的视角实现类称为不受监管的视角。这种类型的视角全面验证有关候选 vault 的各种所需属性。通常,它们将要求 vaults 已最终确定,并且没有任何异常或意外的配置。

接下来,集群视角尝试验证每个 vault 配置的抵押品。为此,集群视角使用可接受视角列表,其中可能包括也可能不包括它自身。对于每个抵押品,它递归到每个视角中,一旦一个视角接受抵押品就停止。如果没有一个这样做,视角本身将失败。此方法允许视角将一些决策委托给其他视角。这减少了创建视角所需的工作量,并利用了 vaults 的验证可能已缓存在这些其他视角中的事实。

不受监管的视角有两个子类:“0x”和“nzx”。

0x 视角

在这种情况下,“0x”指的是 zero exposure(零风险),其中 exposure 指的是治理风险。匹配此视角的 Vaults 只能使用匹配以下视角之一的抵押品 vaults:

  • 托管
  • 0x 视角本身

这意味着不仅 0x vault 本身没有治理风险,而且其抵押品或其临时抵押品等也没有治理风险。

Nzx 视角

在某些情况下,0x 视角可能过于约束,并且可能有一组可接受作为抵押品的 vaults,即使它们受到监管。为此,可以使用 non-zero exposure(非零风险)视角。这些 vaults 允许将以下vaults用作抵押品:

  • 受监管的视角
  • 托管
  • Nzx 视角本身

组成 Vaults

抵押品利息

escrow(托管)vaults 作为抵押品,借款人不会从其抵押品中获得任何利息,即使他们为借款支付利息。尤其是在使用杠杆时,利息支付会放大,有抵押品利息抵消借款利息通常是盈利和无利可图活动之间的区别。如果没有抵押品利息,就不可能通过以高收益资产作为抵押品借入低收益资产来套利利率(“carry trade”),从而导致利率市场效率低下。

为了让借款人从其抵押品中获得利息,vault 必须接受计息 vault 作为抵押品。这有时被称为再抵押。通过创建这样做的 vault,可以构建相互抵押的生态系统,例如(原始)Compound 或选择性抵押的生态系统,例如 AAVE/Euler V1。

但是,使用计息 vaults 作为抵押品存在固有风险:当尝试解除头寸(或清算它)时,抵押品可能不可用,因为它已被借出。此外,vault 以传递方式承担其抵押 vault 的 governance and market risks(治理和市场风险)。以托管 vaults 作为抵押品的贷款可以获得更高的 LTV,从而认识到降低的清算风险。

自定义抵押品

可以对 vaults 进行参数化,以接受任何其他 vault 的 shares 作为抵押品,即使它们不被其他 vaults 接受为抵押品。这允许用户表达其多样化的风险偏好,而不必拆分用作风险抵押品 vaults 的流动性。

即使一个 vault 可用作其他 vaults 中的抵押品,专用 vault 也可以指定不同的(更高或更低)LTV 和/或不同的价格预言机。即使抵押品来自同一个 vault,不同的贷款也可以由不同的配置管理。

与往常一样,在接受 vault 作为抵押品之前,贷方必须考虑底层资产和定价来源固有的风险。虽然建议对抵押品使用 trusted vault implementations(受信任的 vault 实现),但可以使用任何 EVC 兼容的合约。有 ERC-20 adaptors(ERC-20 适配器),它们为新代币或现有代币用作抵押品提供了一种简单的方法。

嵌套

可以通过使用计息资产作为底层资产来组合收益。如果此资产恰好是另一个 Euler vault(“eToken”)的 shares 代币,则该 vault 称为 nested(嵌套)。

这可能看起来很深奥,所以让我们从一个非嵌套的例子开始:想象创建一个以 Compound cToken 作为底层资产的 vault,比如 cXYZ。假设 vault 的 shares 代币的符号为 ecXYZ。假设有借款人,ecXYZ 代币将可以随着时间的推移兑换成更多的 cXYZ,因为 vault 的汇率会随着应计利息而增加。

此外,底层的 cXYZ 代币 会随着时间的推移变得更有价值。为了举例说明,假设每个代币在一段时间内将变得更有价值 5%。如果你将你的 XYZ 代币存入 Compound,然后将生成的 cXYZ 代币存入 Euler vault,你将获得 1.05 * 1.05 = 1.1025,或 10.25% 的收益率。

为了设置杠杆头寸,从 ecXYZ vault 借入 cXYZ 的用户然后将从 Compound 提取 XYZ 代币,然后将其交换为借款所需的抵押品。因为此抵押品由 ecXYZ vault 定义,所以它可以是 vault 管理员配置的任何代币,即使它不是正常的抵押品类资产,并且借款也可能以更高的 LTV 进行。这种灵活性是有代价的:在贷款期间,借款人既要向 cXYZ 要向 ecXYZ 存款者支付利息。

嵌套 vault 遵循相同的原则,但是由于 EVC 批处理和闪电流动性,它们使用起来更方便,并且比与完全独立的系统交互更节省 gas。Euler Vaults 的设计允许嵌套用于所有可行的配置,而不会触发重入问题,并且 convertToAssets 可以安全地用作 pricing oracle(定价预言机)。但是,有一种例外情况,即必须避免嵌套:当嵌套 vault 用作责任 vault 的抵押品时。这种配置将触发重入,从而阻止清算发生。

嵌套 vault 可以设置 CFG_EVC_COMPATIBLE_ASSET,这会禁用 vault 使用的保护机制,以确保不将非 EVC 兼容的代币转账到已知的子账户地址,否则这些代币将丢失。

由于 bad debt socialisation(坏账社会化)代表突然但可预测(按 MEV 时间尺度)的价格变化,因此机会主义的市场参与者可以利用嵌套 vault 配置借入和卖空坏账 vault 的 shares。在某些情况下,这可能会对 vault 存款者产生不利影响。 与无抵押存款的情况类似,当账户不健康时,嵌套 vault 上的某些操作可能会受到限制。例如,如果嵌套 vault 上的 repay() 操作不足以使账户恢复健康,则可能会失败,因为当其份额被移动时,父 vault 会安排账户状态检查。

引导启动

嵌套 vault 有助于解决专用 vault 的流动性引导启动问题。向当前不支付利息的新 vault 存款的机会成本至少等于在已建立的 vault 上可能获得的利息。通过使用嵌套 vault,存款人可以继续赚取已建立 vault 的“基础收益”,同时提供流动性给新的 vault。

合成资产 Vault

Euler 合成 vault 是 vault 的一种特殊配置,它使用hooks来禁用除合成资产地址本身之外的所有地址的存款相关操作。合成 vault 不使用基于利用率的利率模型,而是使用反应式利率模型,该模型根据合成资产的交易价格调整利率:IRMSynth。这种机制、Hook稳定模块和储蓄旨在使合成资产尽可能紧密地与Hook资产Hook。

从合成 vault 借款时使用的价格 feed 是合成资产所Hook的资产,从而创建一个基于 CDP 的合成资产。

ESynth

ESynth 是一个兼容 ERC-20 的 token,具有 EVC 支持,这允许它通过检查转账和销毁时的帐户状态,在其他 vault 中用作抵押品。

铸造

合约所有者可以通过为任何地址调用 setCapacity(address minter, uint128 capacity) 来设置铸造容量,这允许他们铸造高达定义金额的合成资产。铸造者通过调用 mint(address account, uint256 amount) 进行铸造。

销毁

任何地址都可以从另一个地址销毁合成资产,前提是他们有这样做的授权。当从 ESynth 合约本身销毁时,所有者不受此限制。当一个地址从另一个地址销毁并且他们有先前铸造的金额时,铸造的金额将减少销毁的金额,从而释放他们的铸造容量。可以通过调用 burn(address account, uint256 amount) 来完成销毁。

分配给 Vault

所有者可以通过调用 allocate(address vault, uint256 amount) 将合成资产本身持有的合成资产分配给 vault,这相当于向合成资产 vault 进行协议存款。任何分配都需要首先由铸造者铸造到合成资产合约中。在分配时,vault 会被添加到地址列表中,在计算 totalSupply 时会忽略这些地址的余额。

从 vault 中撤回分配

所有者可以通过调用 deallocate(address vault, uint256 amount) 从 vault 中撤回合成资产的分配,这相当于从合成资产 vault 中进行协议提款。从 vault 中撤回分配的资产将被转移到合成资产合约本身,并由所有者单独销毁。

总供应量调整

由于协议存款到合成 vault 中不流通,因此它们从 totalSupply 计算中排除。调用 allocate() 后,目标 vault 会自动排除,合成合约本身也是如此。应该忽略其余额的其他地址可以通过所有者调用 addIgnoredForTotalSupply(address account)removeIgnoredForTotalSupply(address account) 来管理。

由于累积的利息由 vault 持有,因此不直接流通,因此也从 totalSupply 计算中排除。如果用户不小心将其合成 token 转移到被忽略的合约,它们也会被视为不流通。例如,不小心将合成资产转移到合成 vault 实际上会销毁它们(就像转移到任何未准备好的地址一样),并将其从 totalSupply 中删除。

请注意,在从合成 vault 执行闪电贷(或实际上是常规借款)时,totalSupply 将反映出借入的金额实际上已在贷款期间进入流通。这可以被认为是合成资产的“闪电铸造”。

IRMSynth

合成资产使用与标准 vault 不同的利率模型。IRMSynth 利率模型是一个简单的反应式利率模型,当其交易价格低于 TARGET_QUOTE 时,利率会向上调整,当其交易价格高于或等于 TARGET_QUOTE 时,利率会向下调整。

参数

  • TARGET_QUOTE IRM 正在定位的价格
  • MAX_RATE 收取的最高利率
  • BASE_RATE IRM 的最低和起始利率
  • ADJUST_AMOUNT 每次调整间隔可以调整先前利率的量。(默认 = 先前利率的 10%)
  • ADJUST_INTERVAL 再次更改利率之前需要经过的时间

机制

  1. 如果调整间隔未经过,则返回先前利率
  2. 如果合成资产的交易价格低于目标报价,则将利率提高 10%(与先前利率成比例)
  3. 如果合成资产的交易价格等于或高于目标报价,则将利率降低 10%(与先前利率成比例)
  4. 强制执行最低基准利率
  5. 强制执行最高利率
  6. 保存更新后的利率以及上次更新的时间

EulerSavingsRate

EulerSavingsRate 是一个兼容 ERC-4626 的 vault,允许用户存入基础资产并以相同的基础资产的形式获得利息。在提款、赎回和转移时,用户的 accountStatus 会通过调用 EVC 进行检查,从而允许它被其他 vault 用作抵押品。

任何地址都可以将基础资产转移到 vault 中并调用 gulp(),这将将其在为期两周的“涂抹”期间分配给 vault 的份额持有人。累积的利息反映在 vault 的 totalAssets() 中,从而相应地调整汇率。在存款和赎回时,累积的利息被添加到内部 _totalAssets 变量中,该变量以抗捐赠攻击的方式跟踪 vault 中的所有存款。

gulp() 上,任何尚未分配的利息都会被涂抹额外两周,从理论上讲,这意味着通过持续调用 gulp() 可以无限期地涂抹利息,实际上,预计利息会继续累积,从而抵消可能来自涂抹机制的任何负面影响。此外,延迟的利息金额会随着时间的推移呈指数级下降。

PegStabilityModule

PegStabilityModule 将被授予 ESynth 的铸造权,并将允许从基础资产到合成资产以及从合成资产到基础资产的无滑点转换。在部署时,会设置转换为合成资产和转换为基础资产的费用。这些费用会累积到 PegStabilityModule 合约中,并且无法提取,从而作为支持Hook的永久储备。

ESynth 中,交换为合成资产的可能性高达分配给 PegStabilityModule 的铸造上限。交换为基础资产的可能性高达 PegStabilityModule 持有的基础资产的数量。

交互模式

Euler Vault 系统严重依赖 EVC 来启用高级用例。与 Euler V1 相比,贷款协议需要更少的专用代码,并且可以更有效地组成操作和条件。

虽然这超出了本文档的范围,但一些示例可能有助于解释为什么 vault 系统的构建方式如此。

交换

一个简单的例子是在订单上放置一个过期时间戳。过去,某些暴露时间敏感操作的函数(最值得注意的是 Swap 模块)中有一个特殊的“expiration”字段。现在不再需要了。相反,可以添加一个 EVC 批处理项,该项调用批处理帮助合约中的一个函数,该合约的唯一目的是在 block.timestamp 大于指定时间戳时失败。

也许最令人惊讶的是,EVC 和 vault 都不需要了解任何关于交换或滑点检查的信息。交换可以直接通过 EVC 的 call 机制执行,或者委托给一个不一定受信任的“交换助手”合约,然后验证收到的资金是否令人满意。

EVC 操作员

EVC 操作员是一种将你帐户的控制权委托给另一个地址的方式。通常,这将是一个具有有限的、专业功能的智能合约。

操作员最明显的直接应用是委托权限以根据链上指标打开、修改或关闭头寸。例如,止损单可以放在订单上,使“keepers”能够在特定价格预言机指示已达到价格水平时关闭你的头寸,以换取执行费用。

无 Gas 交易

EVC 还支持“无 gas 交易”。这些是指定用户想要执行的一批操作的签名消息。当然,有人必须支付 gas 来执行这些消息,因此必须有一种方法来补偿他们。同样,EVC 的通用性可以用于构建一个系统,而无需任何特殊扩展。例如,可以有一个简单的合约,任何人都可以存储自己的地址,然后调用一个回调(获得了重入锁)。执行者会将他们自己的地址放在这个合约中,然后在回调中评估用户的批处理。无 gas 交易应该会向这个地址发送某种补偿。它可以是任何任意 token,包括 vault 份额,只要执行者愿意接受它。

一般来说,EVC 和 Euler Vault 系统旨在实现 keepers 的统一。最终可以运行一个不可知论的机器人,搜索高级交易系统运行所需的所有不同活动中的价值:

  • 清算
  • 无 Gas 交易
  • 止损、止盈、追踪止损等条件单
    • 更新条件单的状态(例如追踪止损价格、一对一取消等)
  • 基于意图的交易系统

附录

ERC-4626 不兼容性

虽然 Euler Vault Kit 尝试尽可能地符合 ERC-4626 标准,但在某些情况下和配置中,已知它是不兼容的,包括:

  • 当一个帐户启用了 controller(即,有活跃的借款)时,调用帐户抵押品 vault 上的 maxWithdrawmaxRedeem 将返回 0。这是因为确定不会导致运行状况违规的最大可提款金额在技术上具有挑战性。此行为违反了标准对这些函数“必须返回[不会]导致 revert 的最大金额”的要求。但是,该标准确实允许 vault 低估,这可以被视为一种极端的低估。
  • 同样,maxDepositmaxMint 会考虑 vault 的供应上限。但是,当检查被延迟时,可能会暂时存入超过这些上限的金额,而这些函数不会考虑到这一点。同样,这是一种技术上允许的标准所允许的低估。
  • 某些方法(例如 depositredeem)会将 type(uint256).max 的数量特殊化为表示当前可用的最大数量。
  • 如果安装了hooks,则标准的 max* 函数可能不准确。在这种情况下,这些函数将返回 vault 代码通常会返回的值,而不会考虑到 hook 可能会根据其自己的自定义逻辑撤消交易。但是,如果 hook 目标是 address(0)(或任何其他非合约),则 hooked 操作将正确返回 0
  • 该标准说某些 view 函数(totalAssetsconvertToSharesmaxDeposit 等)“不得 revert”。但是,Vault Kit 对这些函数(和其他函数)强制执行只读重入保护,以防止外部合约在 vault 操作期间查看不一致的状态。因此,按照设计,当 vault 操作正在进行时,如果 vault 调用外部合约并且它(或它调用的东西)尝试在 vault 上调用这些函数,它们 revert。

ERC-20 不兼容性

由于 ERC-20 是 ERC-4626 的一个子集,因此 EVK 也尝试完全实现此标准。但是,有一些已知的不兼容性:

  • transfer/transferFrom 中,目标地址不能与源地址相同。
  • 转移到 address(0) 将失败。要销毁 token,请使用另一个地址,例如 0xdead
  • 即使为 address(0)address(1) 设置了批准(这在大多数链上应该是不可能的),你仍然无法从这些地址转移。

静态模块

出于代码组织的目的,并且为了在完全优化级别下舒适地保持在代码大小限制以下,组件实现合约被组织成模块。主要入口点合约 EVault 用作确定应调用哪个模块的 dispatcher。EVault 继承自所有模块,但 Solidity 编译器会将调度例程覆盖的函数视为死代码,并且不会将其编译到其中。除了包含在 EVault dispatcher 合约中之外,模块还单独部署,以便可以使用 delegatecall 调用它们。

这种模式允许以以下方式处理函数:

  • 直接实现:不调用外部模块。这是 gas 效率最高的,对于其他合约频繁调用的 view 方法尤其重要。
  • use(MODULE_XXX):使用 delegatecall 调用指示的模块
  • useView(MODULE_XXX):实现合约使用 staticcall 在自身上调用 viewDelegate(),然后使用 delegatecall 调用指示的模块。

为了直接实现一个函数,在 dispatcher 中没有提及它就足够了。但是,为了文档记录和一致性,我们选择通过使用一个包装器覆盖该函数来实现这一点,该包装器使用 super() 调用相应模块的函数。此包装器函数由编译器内联删除。

要将一个函数委托给一个模块,代码使用 useuseView 修饰符和一个空代码块作为实现来覆盖 dispatcher 中的函数签名。空代码块会删除该函数,而修饰符会导致路由器 delegatecall 到该模块中。

模块是静态的,这意味着它们无法升级。代码升级需要部署一个引用新模块的新实现,然后更新工厂中的实现存储槽。只有可升级的实例会受到影响。

Delegatecall 进入 view 函数

Solidity 不允许 view 函数调用 delegatecall。为了能够从 dispatcher 代码库中删除 view 函数,而是 delegatecall 将它们放入模块中,调度机制使用 useView 修饰符。此修饰符使 view 函数 staticcall 返回到 dispatcher 的一个 viewDelegate 函数,该函数是 payable 的(可能会修改状态)。现在,此 viewDelegate 函数可以 delegatecall 进入模块中 view 函数的实现。

为了解决这个问题,这个 solc issue中提供了一个针对 Solidity 编译器的提议补丁。

Gas 与代码大小的权衡

EVault.sol 中的顶层模块分发系统充当一个调度程序,你可以在这个调度程序中混合和匹配代码的物理位置 -- 在合约本身中,或 delegatecall 到其中一个模块。将特定函数路由到特定模块的代码被硬编码到调度程序中。

为了确定应该直接实现一个函数还是应该委托给一个模块,应该评估其 gas 重要性和代码大小。链上频繁调用的函数将受益于直接实现,以避免 delegatecall 开销。对于 view 函数来说,这一点尤其重要,因为 viewDelegate() 开销。另一方面,应该委托大型函数,以确保 EVault 调度程序可以容纳在 24 Kb 代码大小限制内。

CallThroughEVC

EVault 调度程序利用另一个名为 callThroughEVC 的修饰符。此修饰符有两个执行流程。如果调用来自 EVC,它会正常执行函数(分发或直接),否则它会调用 EVC 的 call() 方法回调到 vault 中。这样做的目的是为了使 vault 始终可以假设流动性检查已延迟,即使是直接调用也是如此。

从 EVault 继承

所有公共函数都被标记为 virtual,因此它们可以被希望构建在 EVault 之上的合约轻松覆盖。但是请注意,此类修改需要新的工厂部署,这意味着它们无法保留经过审计的工厂建立的信任根。对于许多目的,hooks可能是一个更好的解决方案,因为它们不需要新的部署 vault 代码。

一些内部函数也被标记为 virtual。这对于子类也可能很有用,但主要目的是供测试套件使用,以在关键内部方法周围添加不变性检查。

数量类型

为了使 Solidity 编译器能够捕获转换错误,并且为了内部文档记录的目的,存储不同类型的数量的变量被赋予了它们自己的包装整数 Solidity 类型。这些类型为舍入和转换提供了正式接口,并强制执行内部存储宽度等限制:

Assets 类型

Assets 数量表示基础资产的数量,由 balanceOf 方法返回。虽然合约在内部不使用它,但对于显示而言,应考虑基础资产 token 的小数位数。

Assets 必须低于 MAX_SANE_AMOUNT,这是最大 uint112 值。根据惯例,大多数合理的资产都符合此限制。例如,Uniswap2 将 token 数量限制为此大小。

可以使用 toSharesDowntoSharesUp 将 Assets 转换为份额,具体取决于所需的舍入行为。

Shares 类型

Shares 表示 vault 自己的份额面额,由其 ERC-20 接口公开(ERC-4626 是 ERC-20 的超集)。每个份额代表对 vault 当前持有的资产数量以及 vault 作为未偿债务所欠的资产的按比例索取权(随着时间的推移,随着利息的累积,这些债务会增加)。

Shares 也必须低于 MAX_SANE_AMOUNT,并且为了显示目的,也应使用基础资产的小数位数。

Owed 类型

Owed 数量表示负债金额。这些金额以与 Assets 相同的面额存储,除了通过左移 INTERNAL_DEBT_PRECISION (31) 位来放加粗。此额外缩放因子用于提高利息累积的准确性,即使利率很小且更新频繁也是如此。

包括缩放在内,Owed 数量适合 uint144,允许它们与 Shares 数量整齐地打包到同一存储槽中,并留下一位来指示用户是否启用了余额转发

AmountCap 类型

AmountCap 使用十进制浮点布局将供应和借款上限表示为 16 位整数。

ConfigAmount 类型

ConfigAmount 是一种通用定点类型,包含按 10,000 缩放的 [0..1] 分数。它编码为 uint16

LTVConfig 类型

LTVConfig 表示 LTV 值,包括计算 LTV 渐变所需的信息。

溢出

金额溢出

如果总供应量、借款和现金等值接近其上限(例如 MAX_SANE_AMOUNT),则 vault 的会计逻辑将变为未定义。在这种情况下,EVK 代码会尝试避免 vault 永久无响应,但不会暗示任何其他行为保证。由于预计这只会发生在总供应量异常高的 token 中,因此主要目标是尽可能有序地结束流动性池。

但是,在这种状态下可能会遇到一些已知的边缘情况。这些边缘情况包括(但不限于):

  • 如果因利息导致超过总借款,则利息累积将暂停,直到部分债务偿还为止。
  • 如果偿还操作会导致总供应量超过最大金额,则偿还将被阻止,直到存款人提取足够的供应量为止。
  • 有关更多示例,请参见审计报告。

利息溢出

Vault 在内部跟踪利息累加器值。每次累积利息时,都会通过将其先前值乘以根据利率和经过的时间计算出的乘数来更新此累加器。由于累加器不断增长,但存储在固定大小的内存位置中,因此最终它将变得太大而无法存储。

幸运的是,EVM 能够存储非常大的数字,我们可以对系统进行参数设置,使其在实际的 vault 配置在其有用寿命内遇到此故障情况的可能性极小。为了能够在不过度限制非常高利率(即由反应式IRM驱动的临时峰值)的可能用例的情况下提供具体的保证,vault 将最大利率限制为大约 1,000,000% APY。此限制还确保 SPY(秒百分比收益率)值可以编码为 uint72

利息计算期间存在两个溢出方面:rpow 指数运算例程和利息累加器本身。

  • rpow 仅用于确定应应用于累加器的乘数,因为上次与 vault 交互。在持续 1,000,000% APY 的情况下,只要每 5 年至少与 vault 交互一次,就不可能发生溢出。
  • 同样,从 vault 创建之日起,在持续 1,000,000% APY 的情况下,利息累加器本身在 5 年内不会溢出。

实际的 vault 当然会有低得多的利率。在持续 500% APY 的情况下,累加器在 29 年内不会溢出。在持续 50% APY 的情况下,130 年。

如果一个 vault 遇到溢出(在 rpow 或其累加器中),累加器将停止增长,这意味着不会再获得/收取任何利息。但是,仍然可以偿还债务和提取资金。请注意,故障情况的行为稍微微妙:尽管它停止增长,但累加器 —— 因此债务和汇率 —— 将首先重置为上次与 vault 交互时的值。

  • 原文链接: github.com/euler-xyz/eul...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
euler-xyz
euler-xyz
江湖只有他的大名,没有他的介绍。