以太坊 Vault 连接器 (EVC)

  • euler-xyz
  • 发布于 2024-07-09 18:12
  • 阅读 50

本文档介绍了以太坊 Vault 连接器 (EVC),它是一个基础层,旨在促进 lending market 所需的核心功能。

Ethereum Vault Connector (EVC)

<!-- END OF TOC -->

Introduction

Ethereum Vault Connector (EVC) 是一个基础层,旨在促进借贷市场所需的核心功能。它作为各种协议的基础构建块,为开发者提供了一个强大而灵活的框架。EVC 主要在 vault 之间进行协调,vault 是实现 ERC-4626 接口的合约,并包含与其他 vault 交互的附加逻辑。EVC 不仅提供了一个通用的基础生态系统,还降低了核心借贷合约的复杂性,使其能够专注于其差异化因素。

为了说明 vault 协调的过程,让我们考虑一个简单的例子。当用户希望借款时,他们必须通过 EVC 将其账户和抵押 vault 链接到借入 vault。每当用户想要执行可能影响账户偿付能力的操作(例如提取抵押品)时,都会咨询负债 vault,也称为“controller”。EVC 负责调用 controller,controller 确定是否允许该操作,或者是否应阻止该操作以防止账户资不抵债。

除了 vault 协调,EVC 还包含构建灵活产品所需的功能,适用于 EOA 和智能合约。以下是基于 EVC 构建的一些好处:

  • 由于统一的流动性和互操作性而产生的网络效应: 参与协议可以接受其他 vault 中的存款,作为适合其 vault 的抵押品,为不再需要将抵押资产从一个协议转移到另一个协议的用户提供便利。
  • 资产属性的灵活性: EVC 不对用作抵押品或负债的资产强制执行特定属性,允许用户创建由不规则资产类别(如 NFT、现实世界资产(RWA)、无抵押 IOU 或合成资产)支持的 vault。
  • 账户流动性检查和 vault 全局约束执行的标准化方法: EVC 允许延迟流动性检查和 vault 状态检查,防止瞬时违规导致失败。EVC 公开了一个接口,该接口从 vault 中抽象出检查的管理。
  • 批量处理: 可以在单个批量操作中执行影响多个 vault 和外部智能合约的多个操作。这对于 UI 用户来说更方便,更节省 gas,并且允许将流动性检查推迟到批量处理结束时。
  • 子账户: 一项功能,允许用户在其单个所有者账户中创建多个隔离的仓位,并在它们之间轻松地重新平衡抵押品/负债,而无需批准,也无需 vault 实现任何特殊逻辑。
  • 运营商: 用户可以附加外部合约来代表子账户行事。这是 token 批准系统的概括,将释放强大的功能,甚至对于 EOA 也是如此。例如,可以将 intents 支持、止损/止盈/追踪止损等修改器添加到仓位,或者可以在其上构建整个分层仓位管理器。
  • 无 gas 交易(元交易): 它们可以开箱即用地支持 EOA 和合约钱包。
  • 模拟: EVC 公开了用于模拟一组操作的效果并在 UI 中为所有 EVC 用户预先可视化其效果的最佳接口。
  • 清算的通用语言: Vault 可以实现一个核心清算接口,这将允许它们依赖现有的清算人网络来确保其存款人的安全。

如前所述,EVC 不仅为通用基础生态系统提供上述功能,还降低了核心借贷合约的复杂性,使其能够专注于其差异化因素,例如定价和风险管理。

Controller

EVC 的主要任务是维护用户与 vault 的自愿关联。通常,用户会将资金存入一个或多个抵押 vault,并为每个打算用作抵押品的 vault 调用 enableCollateral。这会将 vault 添加到给定账户的抵押品集中。用户显然应该小心他们存入哪些 vault,因为恶意的 vault 可能会拒绝退还他们的资金。

在简单地存入和启用抵押品之后,用户不受 EVC 的任何义务或约束,可以自由地从 vault 中提取资金和/或调用 disableCollateral 以从账户的抵押品集中删除 vault。

但是,假设用户想从一个单独的 vault 中借款。在这种情况下,用户必须调用 enableController 以将此 vault 添加到账户的 controller 集中。这是一个重要的操作,因为用户现在完全将账户提交给 controller vault 代码中编码的规则。所有抵押 vault 中的所有资金现在都间接受 controller vault 的控制。特别是,如果用户尝试提取抵押品或 disableCollateral 以从抵押品集中删除 vault,controller 可能会导致交易失败。此外,controller 可以允许没收抵押品以偿还债务,使用 controlCollateral

  • 当请求执行诸如借款之类的操作时,负债 vault 必须调用 EVC 的 isControllerEnabled 函数,以确保账户实际上已启用 vault 作为 controller。
  • 只有 controller 本身才能在 EVC 上调用 disableController。这通常应该发生在账户全额偿还债务时。必须仔细编码 Vault,以避免出现诸如无法偿还的粉尘之类的极端情况,否则账户可能会永久与 controller 关联。
  • 可以使用 reorderCollaterals 更改账户抵押品集的顺序。因为某些 controller vault 会按顺序循环访问账户的抵押品,如果找到了足够的价值,则会提前返回,这可以节省大量的 gas。此功能的灵感来自 Gearbox 的 collateralHints

鉴于启用 controller 会使指定的账户受到 controller 代码中编码的规则的约束,因此用户必须仅启用受信任的、经过审计的 controller。如果 controller 是恶意的或编码不正确,可能会导致用户资金的损失,甚至导致账户无法使用。

Account Status Checks

账户状态检查由 vault 实现,以强制执行账户偿付能力。Vault 必须公开一个外部 checkAccountStatus 函数,该函数将接收账户的地址和此账户的已启用抵押 vault 列表。如果该账户尚未从此 vault 借入任何东西,则该函数应返回一个特殊的 magic 成功值(checkAccountStatus 方法的函数选择器)。否则,vault 应评估特定于应用程序的逻辑,以确定账户是否处于可接受的状态。如果是这样,它应该返回特殊的 magic 成功值,否则抛出异常。

Collateral Validity

checkAccountStatus 回调中,vault 应该检查提供的抵押品列表,并确定它们是否可以接受。Vault 可以将自己限制为一小部分抵押品,或者可以更通用,并允许使用他们可以获得价格的任何资产进行借款。或者,vault 始终可以失败,如果它仅打算作为抵押 vault。

Vault 可以根据自己的偏好(包括负债和接受的抵押品)自由地对所有资产进行定价,而无需依赖可能不可靠的预言机。

虽然 controller 可能会允许各种各样的抵押 vault 来鼓励借款,但这很诱人,但在决定接受哪些 vault 作为抵押品时,controller vault 的创建者必须谨慎。恶意或编码不正确的 vault 可能会(除其他事项外)错误地表示其持有的资产数量,在用户违规时拒绝清算,或在必要时未能要求进行账户状态检查。因此,vault 应将允许的抵押品限制为一组已知的可靠的经过审计的地址,或者在注册表或工厂合约中验证地址,以确保它们是由受信任的、经过审计的合约创建的。

为 controller 选择合适的抵押 vault 时,一个需要关注的领域是评估抵押行为的潜在差异,具体取决于 EVC 执行上下文标志的状态,例如 controlCollateralInProgress其它。虽然 EVC 公开的标志可能会被抵押合约滥用,从而导致系统中产生意想不到的后果,但当使用得当时,它们也提供了增强系统稳定性的机会。

Execution Flow

尽管 vault 本身实现了 checkAccountStatus,但它们无需直接调用此函数。必要时,EVC 将会调用它。相反,在执行任何可能影响账户流动性的操作之后,vault 应该在 EVC 上调用 requireAccountStatusCheck 以安排将来的回调。此外,可能影响单独账户流动性的操作将需要它们自己的 requireAccountStatusCheck 调用。

requireAccountStatusCheck 调用时,EVC 将确定当前执行上下文是否处于 checks-deferrable call 中,如果是,它将延迟检查此账户的状态,直到执行上下文结束。否则,将立即执行账户状态检查。

如果 vault 实现使用重入保护(建议这样做),则应该考虑一个微妙的复杂性。当在没有延迟账户状态检查的情况下(即,直接地,而不是通过 EVC)调用 vault 时,如果它在 EVC 上调用 requireAccountStatusCheck,EVC 将立即回调到 vault 的 checkAccountStatus 函数中。此时,正常的重入保护将因重新进入而失败。为了避免这种情况,vault 可能希望使用 call EVC 函数。

Single Controller

在账户状态检查时,一个账户最多可以有一个 controller。这就是强制执行每个账户只有一个负债的方法。不允许有多个 controller,因为不太可能两个独立的 controller 能够在存在“共享”账户的情况下保持一致的行为。如果确实需要这样做,可以使用所需的共享逻辑创建一个多 controller 的 controller。

尽管在执行账户状态检查时不允许拥有多个 controller,但在这些检查被延迟时可以瞬时附加多个 controller。只要所有或除了一个 controller 之外的所有 controller 在 checks-deferrable call 的执行期间释放了自己,账户状态检查就会成功。

Last Account Status Check Timestamp

对已启用的 controller vault 成功调用 checkAccountStatus 将在 EVC 的存储中记录给定账户的上次成功账户状态检查的时间戳。此时间戳稍后可以使用 EVC 上的 getLastAccountStatusCheckTimestamp 函数检索。

Forgiveness

如果 controller 想要免除对其控制的账户的流动性检查,则可以“原谅”一个账户。这会将它从将在调用的最后进行检查的账户集中删除。controller 只能原谅他们是唯一 controller 的账户。

毋庸置疑,应该谨慎使用此功能。它应该仅在某些高级清算流程中是必要的,在这种流程中,抵押品是从不健康的账户中扣押的,但是扣押资金仍然不足以使账户达到足以通过账户状态检查的足够健康水平。

这样做时,重要的是 vault 验证在扣押期间是否没有其他抵押品意外地被提取,以防 vault 在其 transfer/withdraw/etc 方法中进行任何意外的外部调用。

Vault Status Checks

某些 vault 可能具有应全局强制执行的约束。例如,供应和/或借款上限,限制可以供应或借入的最大资产数量,以尽量减少风险。

在检查账户状态时,强制执行这些检查不一定有意义。首先,如果批量处理中涉及许多账户,则每次都检查这些全局约束将是多余的。

其次,某些类型的检查需要对 vault 状态进行初始快照,然后才能执行任何操作。对于借款上限,可能是由于某种原因超过了借款上限(可能是由于价格变动,或者借款上限本身已降低)。即使偿还不足以使总借款低于借款上限,vault 仍然希望允许偿还债务。

如果 vault 具有需要全局强制执行的约束,则它们可以公开一个外部 checkVaultStatus 函数。在该函数中,vault 应该评估特定于应用程序的逻辑, 以确定 vault 是否处于可接受的状态。如果是这样,它应该返回一个特殊的 magic 成功值(checkVaultStatus 方法的函数选择器),否则抛出异常。

尽管 vault 本身实现了 checkVaultStatus,但它们无需直接调用此函数。必要时,EVC 将会调用它。相反,在执行任何可能影响 vault 状态的操作之后,vault 应该在 EVC 上调用 requireVaultStatusCheck 以安排将来的回调。

在收到 requireVaultStatusCheck 调用时,EVC 将确定当前执行上下文是否延迟检查,如果是,它将延迟到检查状态,直到执行上下文结束。否则,将立即执行 vault 状态检查。

为了评估 vault 状态,checkVaultStatus 可能需要访问 vault 初始状态的快照。如果是这样,则参考 vault 中实现的推荐模式如下:

  • 在执行任何操作之前,每个需要 vault 状态检查的操作首先应该进行适当的快照并将数据存储在临时存储中(如果尚未制作快照)
  • 应该执行操作
  • 然后 vault 应该调用 requireVaultStatusCheck
  • 当调用 checkVaultStatus 回调时,它应该通过解压缩存储在临时存储中的快照数据并将其与 vault 的当前状态进行比较来评估 vault 状态,并返回特殊的 magic 成功值,如果存在违规,则恢复。

与账户状态检查一样,如果 vault 实现使用重入保护(建议这样做),则应该考虑一个微妙的复杂性。当在没有延迟 vault 状态检查的情况下(即,直接地,而不是通过 EVC)调用 vault 时,如果它在 EVC 上调用 requireVaultStatusCheck,EVC 将立即回调到 vault 的 checkVaultStatus 函数中。此时,正常的重入保护将因重新进入而失败。为了避免这种情况,vault 可能希望使用 call EVC 函数。

Execution

Checks-deferrable Call

EVC 公开了多个功能,每个功能都有其自己的特性,这些功能允许延迟 账户状态检查Vault 状态检查callbatchcontrolCollateral 所谓的 checks-deferrable call 函数可以嵌套,并允许将检查延迟到最外层函数调用的执行结束。

call

EVC 上的 call 函数允许用户调用 vault 和其它目标智能合约上的函数,包括 EVC 本身。除非 msg.senderonBehalfOfAccount 相同,否则用户必须通过此函数,而不是直接调用 vault。这是因为 vault 本身不理解子账户或运算符,并且将其授权逻辑推迟到 EVC(请参阅 Vault 认证 部分)。

call 还允许用户调用任意合约,使用任意 calldata。这些其他合约将看到 EVC 作为 msg.sender。因此,至关重要的是,EVC 本身永远不会被赋予任何特殊权限,或者持有任何 token 或原生货币余额(除了少数临时安全的情况,请参见 EVC 合约特权 部分)。

如果目标合约 EVC,为了保留 msg.sender,EVC 将使用 delegatecall 进行自我 call

如果目标合约不是 msg.sender,仅当 msg.sender 是提供的 onBehalfOfAccount 的所有者或运算符时,EVC 才允许调用目标合约。如果满足该条件,EVC 将创建一个上下文,并使用提供的 calldata 和上下文中设置的 onBehalfOfAccount 账户调用目标合约。

因为可以直接调用 vault,而无需通过 EVC,所以在调用它们时可能不会延迟检查。在这种情况下,vault 可以使用 call 函数,以便它们可以假定始终在检查延迟的上下文中执行。如果调用的 vault 指定目标合约为其自身地址(目标合约 msg.sender),EVC 将创建一个上下文,并使用提供的 calldata 和 onBehalfOfAccount 账户(设置为调用 vault 提供的任何内容)回调调用者。vault 应该使用 msg.sender 作为 onBehalfOfAccount。从理论上讲,vault 可以提供任何地址,但是唯一会看到此 onBehalfOfAccount 的另一个合约是 vault 本身:回想一下,仅当 msg.sender 是 EVC 本身时,才应信任 onBehalfOfAccount。为了以这种方式使用 call,建议 vault 在其重入保护修饰符之前使用特殊的修饰符 callThroughEVC。这将负责通过 EVC 路由调用,并且 vault 可以在假定检查始终被延迟的情况下运行。

call 函数还允许转发提供的值(如果指定了最大 uint256,则转发 EVC 的全部余额)。因此,它也可以用于恢复 EVC 中的任何剩余值。

batch

在撰写本文时,公钥/私钥对 Ethereum 账户(EOA)无法在单个交易中直接执行多个操作,除非通过调用智能合约来代表他们执行此操作。EVC 公开了一个 batch 函数,该函数允许一起执行多个操作。这为用户带来了几个优势:

  • 原子性: 用户知道批量处理中的所有操作都将执行,或者都不会执行,因此不存在留下部分或不一致的仓位的风险。
  • 节省 Gas: 如果多次调用合约,那么跨所有调用的“冷”访问成本可以分摊。
  • 状态检查延迟: 有时,批量处理中的多个操作可能需要状态检查,或者执行某些会使账户/vault 处于无效状态的操作,但在批处理中的后续操作中修复此状态可能更方便或更有效。例如,你可能想在一次批量处理中执行提款和借款,或者在存入抵押品之前 借款和交换。通过批量处理,这些检查可以在批量处理结束时执行一次(这也可能更节省 gas)。

call 相同,批量处理可以由对 EVC 本身的调用和外部调用组成。调用 EVC 是用户如何从批量处理中启用抵押品的方式,例如。为了保留 msg.sender,EVC 自我 call 实际上是通过 delegatecall 完成的。

批量处理通常是混合的外部调用,其中一些调用 vault,而另一些调用其他不相关的合约。例如,用户可能会从一个 vault 中提款,然后在 Uniswap 上执行交换,然后存入另一个 vault。每个批量处理项都指定 onBehalfOfAccount,其身份验证规则与 call 相同。

controlCollateral

controlCollateral 函数只能在一种特定情况下使用:当 controller vault 想要代表受其控制的账户在抵押 vault 上调用函数时。这种情况的典型用例是清算。controller vault 会检测到由于价格波动,账户进入违规状态,并没收一些抵押资产以偿还债务。

这是通过 controller vault 调用 controlCollateral 来完成的。它将抵押 vault 作为目标抵押品和违规者作为 onBehalfOfAccount 传入。controller 将使用自己的地址作为 receiver 构建一个 withdraw 调用。从抵押 vault 的角度来看,这看起来像是定期的提款,它不需要知道由于清算而提取了资金。

permit

除了直接调用 EVC 之外,还可以向 EVC 提供名为 permit 的签名消息。Permit 可以由指定的发送者(或在某些情况下由任何人)调用,但它们将代表 permit 消息的签名者执行。它们对于实现“无 gas”交易很有用。

Permit 是 EIP-712 类型的具有以下字段的数据消息:

  • signer: 代表执行操作的地址。
  • sender: 期望执行由 signer 签名的数据的 msg.sender 的地址。
  • nonceNamespacenonce: 用于防止重放 permit 消息和进行排序的值(见下文)
  • deadline: permit 失效的时间戳。
  • value: 期望发送到 EVC 的原生货币的值
  • data: 将用于调用 EVC 的任意 calldata。通常,这将包含对 batch 方法的调用。

Permit 支持两种类型的签名方法:ECDSA,供 EOA 使用,和 ERC-1271,供智能合约钱包使用。在这两种情况下,permit 方法都可以由任何非特权地址(如 keeper)调用。如果签名正好是 65 个字节长,则会尝试 ecrecover。如果恢复的地址与 signer 不匹配,或者对于签名长度不是 65 的情况,则会通过静态调用 signer 上的 isValidSignature 尝试 ERC-1271 验证。

在验证 senderdeadlinesignaturenoncenonceNamespace 之后,data 将用于调用 EVC,转发指定的 value(如果指定了最大 uint256,则转发 EVC 合约的全部余额)。虽然可以调用其他方法,但最通用的方法是使用 batch。在批量处理中,每个批量处理项都可以指定 onBehalfOfAccount 地址。这可以是所有者的任何子账户,这意味着签名的批量处理可以影响多个子账户,就像常规的非 permit 调用 batch 一样。如果 signer 是另一个账户的运算符,则也可以指定另一个账户 - 这对于 gaslessly 调用受限制的“热钱包”运算符非常有用。

在内部,permit 通过 call 调用 address(this) 来工作,这具有将 msg.sender 设置为 EVC 本身的效果,指示 EVC 应该从执行上下文中获取实际经过身份验证的用户。至关重要的是,permit 是发生这种情况的唯一方式,否则可以绕过身份验证。请注意,EVC 可以通过 callbatch 自我调用,但这通过 delegatecall 完成,使 msg.sender 保持不变。

Nonce Namespaces

使用 nonce,Ethereum 交易强制执行交易不能多次被挖掘,并且它们以创建的相同顺序被包含(没有间隙)。

permit 消息包含两个 uint256 字段,可用于强制执行相同的限制:nonceNamespacenonce。每个账户所有者都有一个映射,该映射从 nonceNamespace 映射到 nonce。为了使 permit 消息有效,指定的 nonceNamespace 中的 nonce 值必须等于 permit 消息中的 nonce 字段。使用 permit 会将 nonce 递增 1。

nonceNamespacenonce 的分离允许用户有选择地放松排序限制。所有者可以选择通过几种方式使用命名空间:

  • 始终将 nonceNamespace 设置为 0,并按顺序签署递增的 nonce。这些 permit 消息将像 Ethereum 交易一样运行,并且必须按顺序挖掘,没有间隙。
  • 从 permit 消息中确定性地导出 nonceNamespace(例如,消息字段的哈希值,不包括 nonceNamespace),并始终将 nonce 设置为 0。这些 permit 消息可以以任何顺序挖掘,有些可能永远不会被挖掘。
  • 两者某种组合。例如,用户可以拥有“常规”和“高优先级”命名空间。常规订单将包含在常规序列中,而高优先级 permit 允许绕过此队列。

请注意,任何时候放松排序限制时,用户都应该意识到他们的交易的不同排序可能具有不同的 MEV 潜力,并且他们应该为他们的交易以最不利的顺序(对他们而言)执行做好准备。

可以通过三种方式取消 Permit 消息:

  • 创建具有相同 nonce 的新消息,并使其在不需要的 permit 之前被包含(与 Ethereum 交易一样)。
  • 调用 setNonce 方法。这允许用户将其 nonce 增加到指定的值,从而可能取消过程中许多未完成的 Permit 消息。请注意,不存在使账户无法运行的危险:即使将 nonce 设置为最大 uint256,也有实际上无限数量的其他可用命名空间。
  • 隐式地,通过等待直到 deadline 时间戳过期

Authorisation

在每个 checks-deferrable call 函数中,可以指定一个 onBehalfOfAccount。该函数将确定 msg.sender 是否有权对这个账户执行操作:

  • 如果 msg.sender 以前从未与 EVC 交互,如果它与 onBehalfOfAccount 共享前 19 个字节,那么 onBehalfOfAccount 被认为是 msg.sender子账户,因此 msg.sender 已获得授权。在与 EVC 的第一次交互时,msg.sender 地址存储在 EVC 的存储中,作为一组 256 个具有相同前 19 个字节的账户的所有者。
  • 如果 msg.sender 之前已与 EVC 交互,并且它与 onBehalfOfAccount 共享前 19 个字节,则其地址应与存储在 EVC 的存储中的地址匹配。如果匹配,则已获得授权。
  • 如果 msg.sender 之前已被授权为 onBehalfOfAccount运算符,则已获得授权。
  • 如果 msg.sender 是 EVC 本身,那么这必须来自一个 permit,并且有效的发送者取自执行上下文
  • 在所有其他情况下,调用者无效,整个交易将失败。
Sub-Accounts

子账户允许用户访问彼此完全隔离的多个(最多 256 个)虚拟账户。虽然可以使用多个单独的 Ethereum 地址,但子账户通常更有效和方便,因为它们的操作可以在一个批量处理中组合在一起,而无需设置批准。

由于一个账户一次只能有一个 controller(除了交易中途),子账户也是 Ethereum 账户可以同时持有多个 Vault 借款的唯一方式。

EVC 还维护一个查找映射 ownerLookup,以便可以在链上或链下轻松地将子帐户解析为所有者地址。首次与 EVC 交互时,会填充此映射。为了解析子帐户,应使用子帐户地址调用 getAccountOwner 函数。它将返回帐户的主地址,或者如果该帐户尚未与 EVC 交互,则返回 address(0)

Operators

运算符是比批准更灵活和强大的版本。在生效期间,运算符合约可以代表指定的账户行事。这包括与 vault 交互(即,提取/借入资金),启用 vault 作为抵押品等。因此,建议仅将受信任和经过审计的合约或由受信任的个人拥有的 EOA 安装为运算符。

运算符有许多用例。例如,用户可能希望将修改器(如止损/止盈/追踪止损)安装到帐户中的仓位。为此,可以选择一个特殊的运算符合约,该合约允许“keeper”在满足某些条件时平仓用户的仓位。每个帐户可以安装多个运算符。但是请注意,运算符可能会实现相互矛盾的逻辑,因此在为单个账户安装多个运算符时应格外小心。

运算符类似于 controller,因为账户授予智能合约(可能已经过良好审计)相当大的权限。但是,重要的区别在于账户所有者可以随时撤销运算符的权限,但是他们不能对 controller 执行此操作。相反,controller 必须释放自己的权限。另一个区别是 controller 无法更改账户的抵押或 controller 集,而运算符可以。

Execution Contexts

如上所述,在与 EVC 交互时,通常最好将某些检查延迟到交易结束。这允许用户暂时违反 vault 施加的某些约束,只要约束在交易结束时得到满足即可。

为了实现这一点,EVC 维护一个执行上下文,该上下文将两组地址保存在常规或临时存储中(如果支持):accountStatusChecksvaultStatusChecks。执行上下文还将包含当前已通过身份验证的 onBehalfOfAccount,因此 vault 可以查询它(请参阅 安全注意事项)。

对于 checks-deferrable call 的持续时间,将存在一个执行上下文,然后在将其丢弃。一次只能存在一个执行上下文。但是,允许嵌套调用(见下文)。

当执行上下文结束时,地址集将被迭代:

  • 对于 accountStatusChecks 中的每个地址,确认最多安装了一个 controller(其 accountControllers 集的大小为 0 或 1)。如果安装了 controller,请在此帐户的 controller 上调用 checkAccountStatus,并确保 controller 满意。如果未安装 controller,则不会调用 checkAccountStatus,并且默认情况下认为账户状态有效。因此,必须谨慎使用 disableController
  • 对于 vaultStatusChecks 中的每个地址,请调用存储在集中的 vault 地址的 checkVaultStatus,并确保 vault 满意。

此外,执行上下文包含一些锁,这些锁可以保护关键区域免受重入的侵扰(请参见下文)。

Nested Execution Contexts

如果通过 EVC 调用 vault 或其他合约,并且该合约又重新调用 EVC 以调用另一个 vault/合约,则认为执行上下文已嵌套。但是,执行上下文被视为堆栈。已添加到延迟账户和 vault 状态检查集中,并且仅在展开最终执行上下文后才能对其进行验证。

在内部,执行上下文存储一个 checksDeferred 标志,该标志在每次启动 checks-deferrable call 时设置,并且只有在调用之前其值为 false 时才清除。清除标志后,将执行延迟的检查。嵌套调用很有用,因为否则通过 EVC 调用本身想要通过 EVC 调用其他合约的合约会更复杂。

onBehalfOfAccount 的先前值存储在本地“缓存”变量中,然后在调用目标合约后恢复它。这样可以确保在 msg.sender 为 EVC 时,合约始终可以依赖 onBehalfOfAccount(请参阅 Vault 认证)。但是,当 msg.sender 不是 EVC 时,vault 无法依赖 onBehalfOfAccount,因为它可能已被嵌套上下文更改。

checksInProgress

由于 EVC 调用可能重新进入 EVC(直接或通过它们调用的合约)的 checkAccountStatuscheckVaultStatus 回调,因此 EVC 的执行上下文维护一个 checksInProgress 互斥锁,该锁在展开需要检查的 帐户和 vault 集之前获取。在更改这些集合的操作期间,也会检查此互斥锁。如果它没有这样做,则高级展开函数缓存的信息(例如集的大小)可能会与底层存储不一致,这可用于绕过这些关键检查。

controlCollateralInProgress

抵押控制的典型用例是负债 vault 在清算流程中扣押抵押资产。

但是,当与可能在提款/转移期间调用外部合约的复杂 vault 进行交互时,负债 vault 可能希望确保在扣押期间没有删除其他抵押品。

为了简化此检查的实现,在 controlCollateral 流程中调用抵押 vault 时,controlCollateralInProgress 互斥锁被锁定。锁定后,无法修改任何帐户的抵押或 controller 集。

此外,在抵押控制期间,无法通过 callbatchcontrolCollateralpermit 重新进入 EVC。

Extra Information

执行上下文还指示一些额外的信息,如果合约想要了解有关 EVC 身份验证的额外信息,这些信息可能会很有用。这包括有关模拟和运算符身份验证状态的信息。

Simulations

EVC 还支持在“模拟”模式下执行批量处理。这仅用于“链下”调用,并且对用户界面很有用,因为它们可以向用户显示一系列操作的预期结果。

模拟通过实际执行所请求的操作,然后恢复来工作,这(如果在链上调用)将恢复所有效果。尽管原理很简单,但其中涉及许多设计元素:

  • 中间只读查询可以插入到批量处理中,以收集有用的模拟数据显示
  • 即使状态检查会导致失败,结果仍然可用,例如,以便用户可以准确地看到导致失败的原因
  • 尽管内部模拟通过恢复来工作,但建议的接口将其作为常规返回数据返回,这导致更少的兼容性问题(有时错误数据会被损坏或删除)。这就是 batchRevert 的原因:如果没有外部调用,你将无法执行“try/catch”,因此这必须是一个外部函数,尽管我们建议使用 batchSimulation 入口点。
  • 模拟不会产生使常规批量处理创建大量返回数据的副作用(这会降低 gas 效率)

在模拟中时,EVC 会设置可由外部智能合约观察到的执行上下文 simulationInProgress 标志。虽然引入此标志旨在提高 UX,但必须注意的是,恶意 vault 或外部系统可能会使用此信息在模拟模式期间采取不同的行动,即他们可以欺骗用户认为该 vault/外部系统 不是恶意的。 和其他任何 EVC 功能一样,用户应该只使用具有可信和公认的智能合约的 EVC 模拟,这些合约的目的不是以任何方式欺骗或伤害他们。考虑到 EVC 模拟功能主要供 UI 应用程序使用,这正是应该应用用户保护的地方。如果与批量交互的系统不受信任,则不应将模拟用作确定批量效果的安全措施。如果用户旨在忠实地评估模拟的结果以评估要执行的交易的安全性,他们应该求助于其他方法和可用的商业解决方案。

瞬态存储

为了保持执行上下文,必须从 EVC 的不同调用访问相同的变量。这意味着它们必须保存在存储中,而不是内存中。不幸的是,与内存相比,存储成本很高。幸运的是,EVM 协议可能很快会指定一种新的内存生命周期类型:瞬态存储,它可以被多次调用访问,但访问成本很低。

为了利用瞬态存储,合约的结构是将所有应存储在瞬态存储中的变量保存在一个单独的基类合约 TransientStorage 中。通过在编译时选择性地覆盖它,可以支持新旧网络。

安全考虑

锁定模式

为了提高用户安全性,EVC 引入了 LOCKDOWN MODE锁定模式)。此模式只能由所有者激活,并同时应用于他们的所有帐户。激活后,EVC 会显著降低所有者帐户的功能。在此状态下,所有者仅限于管理 operator(操作员)和 nonce,而 operator 仅限于撤销自己的权限。在 LOCKDOWN MODE锁定模式)激活的情况下,所有者和 operator 都无法在 EVC 上执行任何其他操作。值得注意的是,禁止代表所有者或其任何帐户调用外部智能合约。但是,即使在锁定时,授权的 controller(控制器)仍然可以控制帐户的抵押品。此模式在紧急情况下特别有用,例如,当添加了恶意 operator 或签名了有害的 permit 消息时,需要立即采取行动以保护用户的资产。

禁用 Permit 模式

另一个提高用户安全性的功能是 PERMIT DISABLED MODE禁用 Permit 模式)。此模式只能由所有者激活,并同时应用于他们的所有帐户。激活后,EVC 不再允许执行由激活此模式的所有者签名的 permit。此模式在紧急情况下特别有用,例如,当签名了有害的 permit 消息时,需要立即采取行动以保护用户的资产。

拒绝服务

EVC 的设计目的是作为一个被美化的多重调用合约,允许用户将调用执行到任何其他地址,包括包含恶意代码的合约。与任何其他此类系统一样,用户有责任仔细选择他们与之交互的合约。如果不小心,恶意和不兼容的合约可能会导致拒绝服务攻击,例如,通过请求帐户或金库的状态检查,但检查失败。任意的非恶意代码也可能通过包含超过最大值 10 (SET_MAX_ELEMENTS) 的金库或帐户状态检查次数而导致 EVC 失败。这种行为永远不应对整个系统构成更大的安全威胁,并且在用户的注意下,可以很容易地避免。

通过金库进行身份验证

金库将其身份验证外包给 EVC,但他们自己负责授权。

为了支持子帐户、operator 和能够控制抵押品(即,在清算中),可以通过 EVC 的 callbatchcontrolCollateral 函数调用金库,然后将在金库上执行所需的操作。但是,金库会将 EVC 视为 msg.sender

当金库检测到 msg.sender 是 EVC 时,它应该回调到 EVC,使用 getCurrentOnBehalfOfAccount 检索当前执行上下文。这将告诉金库两件事:

  • onBehalfOfAccount,它指示已通过 EVC 身份验证的帐户。金库应将此视为用于授权目的的 msg.sender 的“真实”值。
  • controllerEnabled,它指示金库当前是否已启用为 onBehalfOfAccount 帐户的 controller。如果金库正在执行需要它作为帐户的 controller 的操作(例如借款),则需要此信息。getCurrentOnBehalfOfAccount 的调用者本身通过 controllerToCheck 参数传递它感兴趣的金库。当 controllerToCheck 设置为零地址时,返回的值始终为 false

EVC 合约权限

由于可以使 EVC 合约使用任意 calldata 调用任何任意目标合约,因此不应授予它任何权限,或持有任何原生货币或代币。

唯一的例外是在批处理中的交易中途。如果一个批处理项将价值或代币临时转移到 EVC 中,但随后的批处理项将其移出,那么只要在此之间没有运行不受信任的代码,就是安全的。但是,将代币转账到 EVC 通常是不必要的,因为可以使用 transferFrom 并通过在合约中设置各种 recipient 参数立即将代币转账到其最终目的地。

一个例外是将 ETH 包装到 WETH 中。存款方法将始终将 WETH 代币记入调用者的帐户。在这种情况下,用户必须在随后的批处理项中转移 WETH(理想情况下,是使用 call 函数存款之后的批处理项)。

重要的是要注意,EVC 中的调用和批处理项可以通过将 value 输入参数设置为 type(uint256).max 来转移 EVC 的整个 value 余额。当调用嵌套时,即使仅存在受信任的合约,这也会带来意想不到的后果。考虑一个用户执行包含三个操作的批处理调用的场景:A、B 和 C。A 将一些 value 提取到 EVC 中,B 在受信任的金库上执行一些任意操作,C 使用 type(uint256).max 作为输入参数将该 value 存入某处。如果 B 在 EVC 上执行使用其 value 余额的操作,那么 C 将无法存入在 A 中收到的预期金额,但该失败通常不会导致 revert。如果 B 触发恶意代码,这可能会出现问题,但当 B 执行的操作正确但还执行了带有 type(uint256).max value 的 EVC 调用时,它也可能会失败,从而无意中使用了来自 A 的所有 value。当在 C 中使用指定的 value 时,不会出现此问题,因为 C 操作会导致 revert。因此,仅当不存在将 value 转移到 EVC 的中间操作时,使用 type(uint256).max 作为 value 才是安全的。

EVC 地址不可信的一个可能导致问题的地方是实现 hook/callback 的代币,例如 ERC-777 代币。在这种情况下,有人可以为 EVC 安装一个 hook 作为 recipient,并导致入站转移失败,甚至可能被重定向。EVC 不会尝试解决此问题,并且在与实现 hook/callback 的合约交互时应小心。

只读重入

EVC 维护的非瞬态存储 可以 在检查被延迟时读取。特别是,这包括为给定帐户注册的抵押品和 controller 列表。

这不应导致“只读重入”问题,因为每个单独的操作都会使这些列表保持一致的状态。特别是,为了释放 controller,该 controller 本身必须调用释放,这通常意味着债务已偿还。

如果外部合约试图读取帐户的抵押品或 controller 状态以强制执行其自身的某些策略,那么用户可能会延迟其流动性检查,偿还贷款,调用外部合约,然后重新获得贷款。在这种情况下,外部合约会将 controller 视为已释放。但是,通过简单地从外部系统获取闪电贷,而不是使用可延迟检查的调用,可以在可延迟检查的调用之外执行相同的操作。

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

0 条评论

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