该UMIP提议在DVM中支持ACROSS-V2价格标识符,用于验证提交给以太坊主网的与桥相关的交易包的有效性。文章详细解释了Across V2的架构、如何构建和验证bundle,包括寻找有效的中继器(relays)、慢中继器以及如何构建PoolRebalanceRoot、RelayerRefundRoot和SlowRelayRoot。此外,还定义了在判断bundle有效性时需要考虑的多个条件。
| UMIP-157 | |
|---|---|
| UMIP 标题 | 添加 ACROSS-V2 作为支持的价格标识符 |
| 作者 | Matt Rice |
| 状态 | 已弃用 (关于 Across v3, 参见 UMIP-179) |
| 创建日期 | 2022/03/30 |
| Discourse 链接 |
除非在 UMIP-179 中另有规定,否则本 UMIP 将被 UMIP-179 取代。
DVM 应该支持 ACROSS-V2 价格标识符。
Across V2 的基本架构是位于以太坊主网上的一个 LP(“流动性提供者”)池,连接到部署在 各种链上的多个“spoke pools”,以方便用户“存款”。存款是从“源”链到不同“目标”链的跨链转移请求,当“relayer”在所需的目标链上向存款人发送其所需的转移金额(扣除费用)时,该请求即被履行。
Relayer 通过 spokes 履行用户的存款来向 Across V2 系统借出资金,并最终由 LP 池偿还。“Bundles”包含许多此类还款,这些还款由 Optimistic Oracle ("OO") 一起验证。除了验证单个还款指令外,OO 还验证再平衡指令,这些指令告诉 LP 池如何将资金转移到 spoke pools 以及从 spoke pools 转移资金,以执行还款并将存款资金从 spoke 转移到 LP 池。
如果没有 relayer 可以为给定的存款请求提供所有资金,则会执行“slow relay”(或“slow fill”),其中资金从 LP 池发送到目标 spoke 以履行存款。这些 slow fill 请求也包含在上述 bundles 中。
Bundles 在链上实现为 Merkle Roots,它唯一地标识特定区块范围内所有还款和再平衡指令的集合。因此,Across V2 通过由 OO 验证的定期 bundles 来转移资金以偿还 relayers 并履行桥接请求。
本 UMIP 准确地解释了如何构建和验证 bundle。

ACROSS-V2 价格标识符旨在供 Across v2 合约使用,以验证提交给主网的一组与桥接相关的 交易是否有效。
注意 1:以下详细信息通常会引用 Across V2 repo, 提交哈希值为:a8ab11fef3d15604c46bba6439291432db17e745。这允许 UMIP 具有恒定的引用,而不是 依赖于不断变化的存储库。
注意 2:在引用“later”或“earlier”事件时,主要排序应基于区块号,次要
排序应基于 transactionIndex,第三级排序应基于 logIndex。有关更多详细信息,请参阅 按时间顺序比较事件 部分。
注意 3:在未指定的情况下,排序应默认为升序,而不是降序。
注意 4:所有事件数据应由至少两个独立的、信誉良好的 RPC 提供商以相同的方式返回,以确保数据的完整性。
智能合约交易可以发出符合这些 docs 的“Returns”部分中描述的规范的事件。具体来说,事件应具有 blockNumber、transactionIndex 和 logIndex 的唯一组合。要按时间顺序比较事件 e1 和 e2,我们可以说
如果 e1.blockNumber < e2.blockNumber,或者如果 e1.blockNumber == e2.blockNumber && e1.transactionIndex < e2.transactionIndex,或者如果 e1.blockNumber == e2.blockNumber && e1.transactionIndex == e2.transactionIndex && e1.logIndex < e2.logIndex,则 e1 比 e2 “更早”。
因此,“earlier”事件具有较低的区块号、交易索引或日志索引,我们应该按该顺序比较事件属性。
可以通过调用 HubPool.proposeRootBundle() 来提出 root bundle,它将发出一个 ProposedRootBundle 事件。
一旦 所有 其 PoolRebalanceLeaves 通过 HubPool.executeRootBundle() 执行,root bundle 才是有效的,该函数只能在提出的 root bundle 的 challengePeriodEndTimestamp 过去后才能调用。
每个存款都会发出一个 quoteTimestamp 参数。此时间戳应在以太坊网络的上下文中进行评估,并且应映射到以太坊区块,该区块的 timestamp 最接近 deposit.quoteTimestamp 但不大于(即 block.timestamp 最接近且 <= deposit.quoteTimestamp)。
RootBundleExecuted 事件和 [PoolRebalanceLeaf] 结构都包含等长的数组:l1Tokens、netSendAmounts、bundleLpFees 和 runningBalances。l1Tokens 中的每个 l1Token 值都是一个地址,对应于部署在以太坊主网上的 ERC20 token。它应映射到其他三个数组(netSendAmounts、bundleLpFees 和 runningBalances)中与数组中相同索引的值。
例如,如果 l1Tokens 是“[0x123,0x456,0x789]”,而 netSendAmounts 是“[1,2,3]”,则地址为“0x456”的 token 的“净发送金额”等于“2”。
ConfigStore 在 globalConfig 中存储一个“VERSION”值。这用于保护 relayers 和数据工作人员在与 Across 交互时免受使用过时代码的影响。“VERSION”应映射到一个整数字符串,该字符串只能随时间增加。“VERSION”通过调用 updateGlobalConfig 来更新,因此它作为链上事件发出。“VERSION”包含的事件的区块时间表示该“VERSION”何时变为活动状态。Relayers 应该支持存款的报价时间戳的版本,否则他们可能会发送无效的 fill。Proposers 和 Disputers 应该支持 bundle 区块范围的最新版本,以验证或提出新的 bundle。此版本独立于 加速存款签名 中包含的版本。
辅助数据只需要一个字段:ooRequester,它应该是从 OO 请求价格的合约。由于该合约应包含足够的信息,供投票者解决 relay 的有效性,因此不需要额外的辅助数据。
示例:
ooRequester:0x69CA24D3084a2eea77E061E2D7aF9b76D107b4f6
以下常量应反映存储在部署在 Etherscan 上的 AcrossConfigStore 合约中的内容。此合约由 Across 治理拥有,并充当以下变量的真实来源。本 UMIP 当前存储在上述合约中的全局变量包括:
CHAIN_ID_INDICES 中。要查询上述任何常量的值,应使用变量名称的十六进制值调用 AcrossConfigStore 合约的 globalConfig(bytes32) 函数。例如,可以通过调用 globalConfig(toHex("MAX_POOL_REBALANCE_LEAF_SIZE")) 来查询“MAX_POOL_REBALANCE_LEAF_SIZE”,这等效于 globalConfig("0x4d41585f504f4f4c5f524542414c414e43455f4c4541465f53495a45")。例如,这可能会返回
“25”
以下常量也存储在 AcrossConfigStore 合约中,但特定于以太坊 token 地址。因此,它们通过查询配置商店的 tokenConfig(address) 函数来获取。
UBar、R0、R1 和 R2。rateModel 类似,这是一个 originChain-destinationChain 键的字典,映射到费率模型,这些费率模型优先于该特定存款路径上 token 的 rateModel。路由费率模型应遵循与默认 rateModel 相同的 UBar、R0、R1、R2 格式例如,查询 tokenConfig("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") 可能会返回:
"{"rateModel":{"UBar":"750000000000000000","R0":"50000000000000000","R1":"0","R2":"600000000000000000"},"spokeTargetBalances":{"1":{"threshold":"200000000000000000000","target":"100000000000000000000"},"42161":{"threshold":"400000000000000000000","target":"200000000000000000000"}}}"
本 UMIP 稍后将解释如何使用全局和 token 特定的配置设置。
ooRequester 地址应为 HubPool 合约 的实例。
如果 ooRequester 中任何预期的详细信息未以预期形式提供,因为 HubPool 不
匹配预期的接口,则标识符应返回 0。
为了获取提案数据,投票者应查找匹配 此签名 的事件在 ooRequester 上。描述此提案的事件是具有最高区块号的匹配事件,该区块号的时间戳小于或等于价格请求的时间戳。如果存在两个都 满足此标准的匹配事件,则可以通过两种方式之一解决。如果时间戳与请求时间戳匹配, 则应使用 earlier 事件。如果时间戳早于请求时间戳,则应使用 later 事件。
从选定的事件中,应该能够收集以下信息:
bundleEvaluationBlockNumberspoolRebalanceRootrelayerRefundRootslowRelayRootbundleEvaluationBlockNumbers 是此 bundle 的每个目标链的结束区块号的有序数组。哪个索引
对应于哪个链由 全局配置 中的 "CHAIN_ID_INDICES" 表示。
要确定每个 chainId 的起始区块号,请搜索最新的
RootBundleExecuted 事件
具有匹配的 chainId,同时仍然早于请求的时间戳。找到该事件后,搜索
ProposeRootBundle
尽可能晚的事件,但早于我们刚刚确定的 RootBundleExecuted 事件。找到此提案事件后,使用 "CHAIN_ID_INDICES" 确定其索引到其 bundleEvaluationBlockNumbers 数组中 chainId 的映射。对于
每个 chainId,它们的起始区块号是此先前 有效提案 事件中该链的 bundleEvaluationBlockNumber + 1 和该链的 latest 区块高度的最小值。
使用最小值允许区块范围处理自上次提案以来链未推进其区块高度的边缘情况,例如当链正在经历已知的硬分叉时。
使用此机制来确定原始
bundleEvaluationBlockNumbers 中表示的每个 chainId 的起始区块号。
请注意,以上规则要求每个 chainId 的 bundleEvaluationBlockNumbers 大于或等于先前 有效提案 的相同 chainId 的 bundleEvaluationBlockNumbers。在链未暂停且以正常频率生成区块的正常情况下,每个提案的区块范围从先前提案的 bundleEvaluationBlockNumbers 加 1 开始,到下一个 bundleEvaluationBlockNumbers。如果 latest 区块高度未超过先前的 bundleEvaluationBlockNumber,则提案的区块范围将从先前提案的 bundleEvaluationBlockNumbers 到相同的数字,即区块范围为 0。
另请注意,如果链 ID 位于 "DISABLED_CHAINS" 列表中,则上述确定结束区块的规则不适用。如果链在特定提案的“主网”结束区块(链 ID 1)中存在于 DISABLED_CHAINS 中,则该链的结束区块应与其在上次有效提案中添加之前的值相同。具体来说,如果链在特定提案的“主网”结束区块(链 ID 1)存在于 DISABLED_CHAINS 中,则该链的结束区块应与其在上次执行的 bundle 中的值相同。
评估
CrossChainContracts
HubPool 合约上的方法(传递每个 chainId)在提案的区块号处确定
SpokePool 合约 的地址对于每个目标链。我们将在下一节中使用这些 SpokePool 地址来查询正确的事件数据。
对于每个目标链,查找其 SpokePool 上介于该链的起始区块号和结束区块号之间的所有
FilledRelay 事件。对于此查询,排除
任何 isSlowRelay 设置为 true 或 fillAmount 等于 0 的 FilledRelay 事件。
对于所有 FilledRelay 事件,可以通过查询
CrossChainContractsSet
来查找存款的源链的 SpokePool 地址,并查找所有匹配的事件,其中 l2ChainId 匹配 FilledRelay 事件中的 originChainId 值。这些事件中的
spokePool 值是此存款可能来自的所有可能的 spoke pools。
我们不能假设使用最新的
SpokePool,这样我们就不会阻止旧的存款被 relay。要使用的实际 spoke pool 是在源链上的存款发送之前在以太坊上发出的最后一个 CrossChainContractsSet 事件中的地址。(我们可以使用 此方法 来识别与存款的 quoteTimestamp 对应的 CrossChainContractsSet 以太坊 block.timestamp)。
注意:在以下部分中,如果 relay 在任何时候被认为是无效的,则在构建 bundle 时不得考虑该 relay。
对于先前找到的每个 FilledRelay 事件,应在 originChainId 的 spoke pools 之一中找到
FundsDeposited
事件,其中以下参数匹配:
amountoriginChainIddestinationChainIdrelayerFeePctdepositIdrecipientdepositor此外,匹配的 relays 应设置其 destinationToken,以便满足以下过程:
FundsDeposited 事件中 [quoteTimestamp] (#[comparing-deposit-events-chronologically-for-different-origin-chains) 或之前具有区块时间戳],其中
originChainId 和 originToken 匹配 destinationChainId 和 destinationToken。从匹配的事件中提取 l1Token 值
。如果没有匹配的事件,则 relay 无效。SetPoolRebalanceRoute 事件中搜索与 quoteTimestamp 之前或之时的相同 l1Token 和 destinationChainId。如果在步骤 1 中找到的事件之后有任何匹配的事件,则 relay 无效。l1Token 值,在 quoteTimestamp 之前或之时使用该 l1Token 和与 FundsDeposited 事件中的 destinationChainId 值匹配的 destinationChainId 搜索最新的 SetRebalanceRoute 事件。如果找到匹配项,则 destinationToken 应匹配 FilledRelay 事件中的 destinationToken 值。如果它们不匹配或者未找到匹配事件,则 relay 无效。要确定 FilledRelay 事件中 realizedLPFeePct 的有效性,使用的过程与
标识符 IS_RELAY_VALID,在 UMIP 136 中指定 中使用的过程完全相同。但是,不使用 RateModelStore 合约来查找存款的费率模型,我们可以使用 AcrossConfigStore 的 tokenConfig 来 查找存款的费率模型。可以通过遵循上面的步骤 2 将已存款的 originToken 映射到 l1Token,该 l1Token 可用于查询 rateModel。
此外,不调用 BridgePool 合约上的 liquidityUtilizationCurrent 和
liquidityUtilizationPostRelay(不传递任何参数)来计算费率模型,而是在 HubPool 合约上调用同名的方法,传入一个参数,即上面 3 步过程中派生的 l1Token。
如果使用这些方法计算出的 realizedLPFeePct 与 FilledRelay 事件中的 realizedLPFeePct 不匹配,则认为 relay 无效。
所有有效的 FilledRelay 事件应然后存储起来以构建 bundle。
要确定所有 slow relay,请按照以下过程操作:
FilledRelay 事件,按 originChainId 和 depositId 对它们进行分组。totalFilledAmount 等于 amount 的 FilledRelay 事件的所有组。这将删除已 100% filled 的存款。filledAmount 非零且等于 totalFilledAmount 的事件的所有组。这仅保留最早的 fill 在此时段内的存款。对于所有剩余的组,它们应存储在 slow relay 组的列表中。
对于 上面 识别的给定 slow relay,我们可以将关联的存款的“未 fill 金额”计算为 deposit.amount - latestFill.totalFilledAmount,其中 latestFill 是存款按时间顺序排列的最后一次 fill。由于每次 fill 都会增加 totalFilledAmount,因此也可以通过对与存款关联的所有 fill 进行排序并保留具有最大 totalFilledAmount 的 fill 来识别 latestFill。
注意:由于我们消除了所有 totalFilledAmount == deposit.amount 的 fill,剩余的“最后一次 fill”应具有 totalFilledAmount < deposit.amount 且具有 totalFilledAmount > [所有其他存款的 fill].totaFilledAmount。
要构建 poolRebalanceRoot,你需要形成一个再平衡列表。
对于上面的所有有效 FilledRelay 事件,按 repaymentChainId 和上面找到的关联的 l1Token 对它们进行分组。
对于每个组,将 fillAmount 值相加,以获得该组的总 relay 还款。
类似地,将 fillAmount * realizedLPFeePct / 1e18 相加,以获得该组的总 LP 费用。
要确定修改运行余额的金额:
FilledRelay 组,在 0 处初始化运行余额值,并将总 relay 还款添加到
它。每个运行余额值由其 repaymentChainId 和 l1Token 定义。SpokePool 的区块范围内的所有 SpokePool 上找到所有 FundsDeposited 事件。使用
originChainId、originToken、quoteTimestamp 和 HubPool 上的 SetPoolRebalanceRoute 事件,使用
类似于上面的 3 步过程将其映射回 l1Token。对于该 l1Token 和 originChainId,
如果运行余额值尚不存在,则初始化一个运行余额值并从中减去 amount。SpokePool 的情况下,但在 slow fill leaf 可以执行之前,relayer 部分地“快速” fill 了存款。之后,执行 slow fill leaf 以完成存款。SpokePool 现在有多余的 token 金额,因为原始 slow fill 支付未完全用于完成存款,因此必须将此超额返回给 Mainnet。因此,此步骤解释了如何识别超额并确定发送回多少(即从运行余额中减去)。查找区块范围内将 isSlowRelay 设置为 true 的所有 FilledRelay 事件。对于每个事件,使用与上面类似的方法 在 quoteTimestamp 处将此事件映射回 l1Token。使用 destinationChainId 作为 repaymentChainId
以确定此事件应应用于哪个运行余额。对于每个先前 验证的 bundle,请按照此 originChainId 和 depositId 的 “查找 Slow Relay” 部分中的步骤操作,并查找具有匹配的
originChainId 和 depositId 的 slow relay。应该只有一项 slow relay 支付与所讨论的 FilledRelay 匹配。这是包含在先前 bundle 中的 slow fill,该 slow fill 已添加到 runningBalance 并最终导致 bundle 将 slow fill 支付发送到 destinationChainId 上的 SpokePool。计算旧 root bundle 中发送的 slow fill 金额。在此当前 FilledRelay(isSlowRelay = true)之后的 SpokePool 中剩余的超额金额等于 slow fill 金额 减去 FilledRelay.fillAmount。换句话说,如果 FilledRelay.fillAmount 小于最初在先前 bundle 中发送的 slow fill 金额,则发送回差额。从关联的 l1Token 和 destinationChainId 的运行余额中减去结果。totalFilledAmount 等于 amount(即完成存款的 fills)且 fillAmount 小于 amount(即不是存款的第一次 fill)的所有 FilledRelay 事件。如果存款的第一次 fill 完成了存款(fillAmount == amount 且 totalFilledAmount == amount),那么 spoke pool 中就不会有剩余的金额,因为这不会触发将 slow fill 支付发送到 spoke。首先我们需要查看此当前 fill 的第一次 fill 是否触发了 slow fill。在先前 验证的 bundle 中,识别同一 originChainId 和 depositId 的所有匹配的 fill。查找最早的此类 fill。使用 此逻辑 获取包含此 fill 的 root bundle 提案的区块范围,以获取 ProposeRootBundle 事件,其中 FilledRelay.destinationChainId 的 bundleEndBlock 大于或等于 FilledRelay 区块号。在此同一 bundle 区块范围内查找最后一次 fill。该 bundle 的 slow fill 支付应为 FilledRelay.amount - FilledRelay.totalFilledAmount,与 此计算 相同。由于我们知道此即将到来的提案中的当前 FilledRelay 已完全 100% 地 fill 了存款,因此我们知道 slow fill leaf 无法执行,因此必须将整个 slow fill 支付发送回主网。从此运行余额中减去此金额(有效 root bundle 中此 fill 最终完成的上一次 slow fill 支付)。我们现在需要将先前的运行余额值添加到给定 repaymentChainId 和 l1Token 的当前值。
对于每个 repaymentChainId 和 l1Token 组合,应查询较旧的
RootBundleExecuted 事件
以查找先前的 RootBundleExecuted 事件。这意味着识别具有与 repaymentChainId 匹配的 chainId 的最新 RootBundleExecuted 事件并 识别 l1Token 索引处的 runningBalanceValue。
对于 l1Token 和 repaymentChainId 的每个元组,我们应该已经计算出总运行余额值。以下算法描述了计算运行余额和净发送金额的过程:
spoke_balance_threshold = 此 token 的 `spokeTargetBalances` 中的“threshold”值
spoke_balance_target = 此 token 的 `spokeTargetBalances` 中的“target”值
net_send_amount = 0
## 如果运行余额为正数,则 hub 欠 spoke 资金。
if running_balance > 0:
net_send_amount = running_balance
running_balance = 0
## 如果运行余额为负数,则从 spoke 中提取足够的资金到 hub,以将运行余额恢复到其目标
else if abs(running_balance) >= spoke_balance_threshold:
net_send_amount = min(running_balance + spoke_balance_target, 0)
running_balance = running_balance - net_send_amount
获取上述运行余额和净发送金额,并仅按 repaymentChainId 对它们进行分组并按 repaymentChainId 排序。在
每个组中,按 l1Token 排序。如果有超过 MAX_POOL_REBALANCE_LEAF_SIZE 个 l1Token,则特定链的 leaf 将
需要拆分为多个 leaf,从 groupIndex 0 开始,每个后续 leaf 将 groupIndex 值递增 1。
现在我们有了排序的 leaf,我们可以为每个 leaf 分配一个唯一的 leafId,从 0 开始。
有了所有这些信息,应该可以以 此处 给出的格式构建每个 leaf。
重要的是,l1Tokens、bundleLpFees、netSendAmounts 和 runningBalances 数组都应具有相同的长度。后三个数组是映射到同一索引的 l1Tokens 条目的值。有关更好地解释如何将 l1Tokens 映射到其他三个数组,请参阅 此章节。
构建 leaf 后,可以通过使用 Solidity 的标准过程 keccak256(abi.encode(poolRebalanceLeaf)) 对每个 leaf 数据结构进行哈希处理来构建 merkle root。对 leaf 进行哈希处理后,应以标准方式构建树,
以便可以使用
OpenZeppelin 的 MerkleProof
库进行验证。有关如何构建这些类型的树的示例,请参见 [此处](https://github.com/OpenZeppelin/openzeppelin-contracts/`refundAmounts和refundAddresses仅仅是通过按照relayer对这个组中的中继进行分组,并将每个中继的amount - (amount * lpFeePct / 1e18)相加计算出来的。这些应该按照refundAmounts的降序排列。如果两个refundAmounts相等,那么应该按照relayer` 地址排序。
如果对于特定的 l2TokenAddress 存在多于 MAX_RELAYER_REPAYMENT_LEAF_SIZE 个 refundAddresses,那么这些应该被拆分成 MAX_RELAYER_REPAYMENT_LEAF_SIZE 个元素的叶子(按照上述方式排序),只有特定 l2TokenAddress 的第一个叶子能够包含非零的 amountToReturn。
一旦这些为所有中继计算出来,那么叶子(或者对于 > 25 个元素的组)应该按照 chainId 作为主索引,然后是 l2TokenAddress 作为辅助索引,再然后是 > MAX_RELAYER_REPAYMENT_LEAF_SIZE 个元素的组的单独排序,作为第三级排序。一旦这些被排序,每个叶子可以根据其在组中的索引获得一个 leafId,从 0 开始。
一旦这些叶子被构建,它们可以被用来形成一个 merkle 根,就像在前一节中描述的那样。
为了构建如 这里 所述的 SlowRelayRoot 叶子,只需要基于在上面的 "Finding Slow Relays" 章节中找到的所有慢中继形成叶子。中继中的信息应该直接映射到叶子数据结构。
它们的主排序索引应该是 originChainId,次排序索引应该是 depositId。
然后你可以构建一个 merkle 根,类似于前两节中的做法。
必须满足三个条件才能认为提案有效:
bundleEvaluationBlockNumbers 必须包含所有在提案时具有非零
CrossChainContractsSet
的 chainIds。如果提案被认为是无效的,返回 0。如果有效,返回 1。注意:这些值按 1e18 缩放。
- 原文链接: github.com/UMAprotocol/U...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!