该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 事件。
从选定的事件中,应该能够收集以下信息:
bundleEvaluationBlockNumbers
poolRebalanceRoot
relayerRefundRoot
slowRelayRoot
bundleEvaluationBlockNumbers
是此 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
事件,其中以下参数匹配:
amount
originChainId
destinationChainId
relayerFeePct
depositId
recipient
depositor
此外,匹配的 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!