该UMIP提议在DVM中支持ACROSS-V2价格标识符,用于验证提交到以太坊主网的桥接相关交易bundle的有效性。文章详细描述了Across V2的架构、数据规范、实现以及如何构建和验证bundle,包括PoolRebalanceRoot、RelayerRefundRoot和SlowRelayRoot的构建方法。同时还涉及到如何确定区块范围,寻找有效的Relay,以及处理慢速Relay等问题。
UMIP-157 | |
---|---|
UMIP 标题 | 添加 ACROSS-V2 作为支持的价格标识符 |
作者 | Matt Rice |
状态 | 已批准 |
创建时间 | 2022/03/30 |
Discourse 链接 |
DVM 应该支持 ACROSS-V2 价格标识符。
Across V2 的基本架构是一个位于以太坊主网上的单一 LP ("Liquidity Provider",流动性提供者) 池,连接到部署在各种链上的多个 "spoke pools"(分支池),以方便用户 "deposits"(存款)。存款是从 "origin"(源)链到不同 "destination"(目标)链的跨链转移请求,当 "relayer"(中继器)在用户所需的目标链上发送给存款人他们想要的转移金额(扣除费用后)时,该请求便被履行。
中继器通过 spokes 履行用户的存款来向 Across V2 系统借出资金,并最终由 LP 池偿还。"Bundles"(捆绑包)包含许多此类还款,由 Optimistic Oracle ("OO",乐观预言机) 一起验证。除了验证单个还款指令外,OO 还验证再平衡指令,这些指令告诉 LP 池如何将资金转移到 spokes 池以及从 spokes 池转移资金,以便执行还款并将存入的资金从 spoke 池提取到 LP 池。
如果没有中继器可以为给定的存款请求提供所有资金,则会执行 "slow relay"(慢速中继)(或 "slow fill"(慢速填充)),其中资金从 LP 池发送到目标 spoke 池以完成存款。这些慢速填充请求也包含在上述捆绑包中。
捆绑包在链上实现为 Merkle Roots,它唯一地标识了特定区块范围内所有还款和再平衡指令的集合。因此,Across V2 通过定期的捆绑包来转移资金以偿还中继器并满足桥接请求,所有这些都由 OO 验证。
此 UMIP 准确解释了如何构建和验证捆绑包。
ACROSS-V2 价格标识符旨在供 Across v2 合约使用,以验证提交到主网的一组与桥接相关的交易是否有效。
注 1:以下详细信息通常会引用 Across V2 仓库,提交哈希值为:a8ab11fef3d15604c46bba6439291432db17e745。这允许 UMIP 具有恒定的引用,而不是 依赖于不断变化的存储库。
注 2:当引用 "later"(较晚)或 "earlier"(较早)的事件时,主要排序应基于区块号,次要排序应基于 transactionIndex
,第三级排序应基于 logIndex
。有关更多详细信息,请参阅 比较事件 的部分。
注 3:在未指定的情况下,排序应默认为升序,而不是降序。
注 4:所有事件数据应由至少两个独立的、信誉良好的 RPC 提供商完全相同地返回,以确保数据的完整性。
智能合约交易可以发出符合这些 文档 的 "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"(更早)。
因此,"earlier"(更早)的事件具有较低的区块号、交易索引或日志索引,我们应该按该顺序比较事件属性。
可以通过调用 HubPool.proposeRootBundle()
来提出根捆绑包,这将发出一个 ProposedRootBundle
事件。
一旦 all(所有) PoolRebalanceLeaves
通过 HubPool.executeRootBundle()
执行,根捆绑包才有效,这只能在提出的根捆绑包的 challengePeriodEndTimestamp
过去后才能调用。
每个存款都会发出一个 quoteTimestamp
参数。此时间戳应在以太坊网络的上下文中进行评估,并且应映射到以太坊区块,该区块的 timestamp
最接近 deposit.quoteTimestamp
但不大于(即 block.timestamp
最接近且 <= deposit.quoteTimestamp
)。
RootBundleExecuted
事件和 [PoolRebalanceLeaf
] 结构都包含等长的数组:l1Tokens
、netSendAmounts
、bundleLpFees
和 runningBalances
。l1Tokens
中的每个 l1Token
值都是一个地址,对应于部署在以太坊主网上的 ERC20 代币。它应该映射到其他三个数组(netSendAmounts
、bundleLpFees
和 runningBalances
)中任何一个的值,这些数组在数组中共享相同的索引。
例如,如果 l1Tokens
是 "[0x123,0x456,0x789]",netSendAmounts
是 "[1,2,3]",则地址为 "0x456" 的代币的 "net send amount"(净发送金额)等于 "2"。
ConfigStore
在 globalConfig
中存储一个 "VERSION"(版本)值。这用于保护中继器和数据工作者在与 Across 交互时免于使用过时的代码。"VERSION" 应该映射到一个整数字符串,该字符串只能随着时间的推移而增加。通过调用 updateGlobalConfig
来更新 "VERSION",因此它会作为链上事件发出。包含 "VERSION" 的事件的区块时间指示该 "VERSION" 何时变为活动状态。中继器应支持存款报价时间戳时的版本,否则他们可能会发送无效的填充。提案者和争议者应支持捆绑包区块范围的最新版本,以验证或提出新的捆绑包。此版本独立于 加速存款签名 中包含的版本。
辅助数据只需要一个字段:ooRequester
,它应该是从 OO 请求价格的合约。因为该合约应该包含足够关于请求的信息供投票者解决中继的有效性,所以不需要额外的辅助数据。
例子:
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
合约中,但特定于以太坊代币地址。因此,它们通过查询配置存储的 tokenConfig(address)
函数来获取。
rateModel
结构包含以下所有参数时才有效:UBar
、R0
、R1
和 R2
。rateModel
类似,这是一个 originChain-destinationChain
键的字典,映射到对该特定存款路由上的代币优先于 rateModel
的利率模型。路由利率模型应遵循与默认 rateModel
相同的 UBar
、R0
、R1
、R2
格式"target"
和 "threshold"
。例如,查询 tokenConfig("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
可能会返回:
"{"rateModel":{"UBar":"750000000000000000","R0":"50000000000000000","R1":"0","R2":"600000000000000000"},"spokeTargetBalances":{"1":{"threshold":"200000000000000000000","target":"100000000000000000000"},"42161":{"threshold":"400000000000000000000","target":"200000000000000000000"}}}"
稍后,此 UMIP 将解释如何使用全局和代币特定的配置设置。
ooRequester 地址预计是 HubPool 合约 的一个实例。
如果 ooRequester 中的任何预期细节无法以预期形式获得,因为 HubPool 不匹配预期的接口,则标识符应返回 0
。
为了获得提案数据,投票者应该找到匹配 此签名 的事件在 ooRequester 上。描述此提案的事件是具有最高区块号的匹配事件,其时间戳小于或等于价格请求的时间戳。如果有两个都满足此标准的匹配事件,那么可以通过两种方式之一解决。如果时间戳匹配请求时间戳,那么要使用的事件是 earlier event。如果时间戳早于请求时间戳,则应使用 later event。
从所选事件中,应该能够收集以下信息:
bundleEvaluationBlockNumbers
poolRebalanceRoot
relayerRefundRoot
slowRelayRoot
bundleEvaluationBlockNumbers
是此捆绑包的每个目标链的结束区块号的有序数组。哪个索引
对应于哪个链由 global config 中的 "CHAIN_ID_INDICES" 表示。
为了确定每个 chainId 的起始区块号,请搜索最新的
RootBundleExecuted event
与 chainId
匹配,同时仍然早于请求的时间戳。一旦找到该事件,搜索
ProposeRootBundle
尽可能晚的事件,但早于我们刚刚确定的 RootBundleExecuted 事件。一旦找到此提案事件,使用 "CHAIN_ID_INDICES" 确定其索引到 chainId
的映射在其 bundleEvaluationBlockNumbers
数组中。对于
每个 chainId
,它们的起始区块号是此先前 valid proposal 事件中该链的 bundleEvaluationBlockNumber + 1
和该链的 latest
区块高度的最小值。
使用最小值允许区块范围处理链自上次提案以来尚未提高其区块高度的边缘情况,例如当链正在经历已知的硬分叉时。
使用此机制确定原始
bundleEvaluationBlockNumbers
中表示的每个 chainId
的起始区块号。
请注意,上述规则要求每个 chainId
的 bundleEvaluationBlockNumbers
大于或等于先前 valid proposal's 相同的 chainId
的 bundleEvaluationBlockNumbers
。在链未暂停并且以正常频率生成区块的正常情况下,每个提案的区块范围从先前提案的 bundleEvaluationBlockNumbers
加 1 开始,到下一个 bundleEvaluationBlockNumbers
结束。如果在先前的 bundleEvaluationBlockNumber
之后 latest
区块高度没有前进,那么提案的区块范围将从先前的提案的 bundleEvaluationBlockNumbers
到相同的数字,即 0 的区块范围。
另请注意,如果链 ID 位于 "DISABLED_CHAINS" 列表中,则上述确定结束区块的规则不适用。如果一个链存在于 DISABLED_CHAINS 中,该拟议的捆绑包必须重复使用添加之前最后一个有效提案中的捆绑包结束区块。具体来说,如果一个链在特定提案的 "mainnet" 结束区块(链 ID 1)上存在于 DISABLED_CHAINS 中,那么该链的结束区块应与其在最新执行的捆绑包中的值相同。
评估
crossChainContracts
HubPool 合约上的方法(传递每个 chainId
)在提案的区块号上确定地址
对于
SpokePool contract
对于每个目标链。我们将在下一节中使用这些 SpokePool 地址来查询正确的事件数据。
对于每个目标链,找到所有
FilledRelay events
在其 SpokePool
上,介于起始区块号和该链的结束区块号之间。对于此查询,排除
任何 FilledRelay
事件,其中 isSlowRelay
设置为 true
或 fillAmount
等于 0
。
对于所有 FilledRelay
事件,可以通过查询
CrossChainContractsSet
并在所有匹配事件中找到 SpokePool
地址,其中 l2ChainId
匹配 FilledRelay 事件中的 originChainId
值。这些事件中的 spokePool
值都是此存款可能来自的可能的 spoke 池。
我们不能假设使用最新的
SpokePool
,这样我们就不会阻止旧的存款被中继。要使用的实际 spoke 池是发生在以太坊上发送存款之前的最后一个 CrossChainContractsSet
事件中的地址。(我们可以使用 这种方法 通过存款的 quoteTimestamp
来确定 CrossChainContractsSet
以太坊 block.timestamp
)。
注意:在下面的章节中,如果在任何时候中继被认为是无效的,在构建捆绑包时不得考虑该中继。
对于早些时候找到的每个 FilledRelay
事件,
应该在 originChainId 的其中一个 spoke 池中找到
FundsDeposited
事件,其中以下参数匹配:
amount
originChainId
destinationChainId
relayerFeePct
depositId
recipient
depositor
此外,匹配的中继应设置其 destinationToken
,以满足以下过程:
FundsDeposited
事件中,quoteTimestamp
或之前的一个区块时间戳,其中
originChainId
和 originToken
匹配 destinationChainId
和 destinationToken
。从匹配的事件中提取 l1Token
值。如果没有匹配的事件,则中继无效。SetPoolRebalanceRoute
事件中搜索与 quoteTimestamp
之前或之后的相同 l1Token
和 destinationChainId
。如果在步骤 1 中找到的任何事件晚于步骤 1 中找到的事件,则中继无效。l1Token
值,在 quoteTimestamp
之前或之后的 l1Token
中搜索最新的 SetRebalanceRoute
事件,并使用匹配 FundsDeposited
事件中 destinationChainId
值的 destinationChainId
。如果找到匹配项,则 destinationToken
应匹配 FilledRelay
事件中的 destinationToken
值。如果它们不匹配或未找到匹配事件,则中继无效。要确定 FilledRelay
事件中 realizedLPFeePct
的有效性,使用的过程与
标识符 IS_RELAY_VALID
(在 UMIP 136 中指定) 中的过程完全相同。但是,我们不是使用 RateModelStore
合约来查找存款的费率模型,而是可以使用 AcrossConfigStore
的 tokenConfig
来 lookup the rate model 查找存款的费率模型。可以通过遵循上述步骤 2 将存入的 originToken
映射到 l1Token
,这可用于查询 rateModel
。
此外,不在 BridgePool
合约上调用 liquidityUtilizationCurrent
和
liquidityUtilizationPostRelay
(不传递任何参数)来计算费率模型,而是在 HubPool
合约上调用相同名称的方法,传入一个参数,即上述 3 步过程中导出的 l1Token
。
如果使用这些手段计算出的 realizedLPFeePct
与
FilledRelay
事件中的 realizedLPFeePct
不匹配,则认为中继无效。
然后应存储所有有效的 FilledRelay
事件以构建捆绑包。
要确定所有慢速中继,请遵循以下过程:
FilledRelay
事件,请按 originChainId
和 depositId
对其进行分组。FilledRelay
事件的组,其中 totalFilledAmount
等于 amount
。这将删除已 100% 填充的存款。filledAmount
非零且等于 totalFilledAmount
的事件的组。这将仅保留最早的填充在此时间范围内的存款。对于所有剩余的组,应将其存储在慢速中继组的列表中。
对于 above 标识的给定慢速中继,我们可以将相关存款的 "unfilled amount"(未填充金额)计算为 deposit.amount - latestFill.totalFilledAmount
,其中 latestFill
是存款按时间顺序排列的最后一次填充。由于每次填充都会递增 totalFilledAmount
,因此也可以通过对与存款关联的所有填充进行排序并保留具有最大 totalFilledAmount
的填充来识别 latestFill
。
注意:由于我们消除了所有 totalFilledAmount == deposit.amount
的填充,因此剩余的 "last fill" 应具有 totalFilledAmount < deposit.amount
AND 具有 totalFilledAmount > [所有其他用于存款的填充].totaFilledAmount
。
要构建 poolRebalanceRoot
,你需要形成一个 rebalances(再平衡)列表。
对于上述所有有效的 FilledRelay
事件,请按 repaymentChainId
及其关联的 found above 的 l1Token
对其进行分组。
对于每个组,将 fillAmount
值相加,以获得该组的总中继还款额。
同样,将 fillAmount * realizedLPFeePct / 1e18
相加以获得该组的总 LP 费用。
要确定修改运行余额的金额:
FilledRelay
group,以 0 初始化一个运行余额值,并将总中继还款额添加到
它。每个运行余额值由其 repaymentChainId
和 l1Token
定义。SpokePool
的区块范围内,找到所有先前 SpokePool
上所有 FundsDeposited
事件。使用
originChainId
、originToken
、quoteTimestamp
和 HubPool 上的 SetPoolRebalanceRoute
事件,使用与上述步骤 3 类似的过程将其映射回 l1Token
。对于该 l1Token
和 originChainId
,
如果运行余额值尚不存在,则初始化一个运行余额值,并从中减去 amount
。SpokePool
发送了慢速填充付款,但是在慢速填充 leaf 可以执行之前,中继器部分地 "fast"(快速)填充了存款。之后,执行慢速填充 leaf 以完成存款。现在,SpokePool
具有过多的代币额,因为最初的慢速填充付款未完全用于完成存款,因此必须将此剩余额退回给主网。因此,此步骤说明了如何识别剩余额并确定发送回多少(即从运行余额中扣除)。找到区块范围内所有将 isSlowRelay
设置为 true 的 FilledRelay
事件。对于每个事件,使用与上面类似的方法 map this event back to an l1Token
at the quoteTimestamp
。使用 destinationChainId
作为 repaymentChainId
以确定此事件应应用于哪个运行余额。对于每个先前 validated bundle,按照 "Finding Slow Relays" 部分中的步骤
对于此 originChainId
和 depositId
,并查找具有匹配的
originChainId
和 depositId
的慢速中继。应该完全有一个与 FilledRelay
匹配的慢速中继付款。这是包含在先前捆绑包中添加到 runningBalance
并最终导致捆绑包向 destinationChainId
上的 SpokePool
发送慢速填充付款的慢速填充。compute the slow fill amount 计算在旧的根捆绑包中发送的慢速填充金额。在此当前 FilledRelay
(其中 isSlowRelay = true
)之后,SpokePool
中剩余的剩余额等于 slow fill amount
减去 FilledRelay.fillAmount
。换句话说,如果 FilledRelay.fillAmount
小于最初在先前捆绑包中发送的 slow fill amount
,则发送回差额。从中减去结果
关联的 l1Token
和 destinationChainId
的余额。totalFilledAmount
等于 amount
(即完成存款的填充)并且 fillAmount
小于 amount
(即不是存款的第一次填充的填充)的 FilledRelay
事件。如果存款的第一次填充完成了存款(fillAmount == amount
并且 totalFilledAmount == amount
),则 spoke 池中不可能有剩余额,因为这不会触发向 spoke 发送慢速填充付款。首先,我们需要查看此当前填充的第一次填充是否触发了慢速填充。在先前 validated bundles 中,确定所有相同的 originChainId
和 depositId
的匹配填充。找到最早的此类填充。使用 this logic 确定包含 FilledRelay.destinationChainId
的 bundleEndBlock
大于或等于 FilledRelay
区块号的 ProposeRootBundle
事件的根捆绑包提案的区块范围。在此同一捆绑包区块范围内找到最后一次填充。捆绑包的慢速填充付款应为 FilledRelay.amount - FilledRelay.totalFilledAmount
,与 this calculation 相同。由于我们知道此即将到来的提案中的当前 FilledRelay
100% 填充了存款,因此我们知道无法执行慢速填充 leaf,因此必须将整个慢速填充付款发送回主网。从运行余额中扣除此金额(来自有效根捆绑包的先前慢速填充付款,该填充最终完成了此填充)。现在,我们需要将先前的运行余额值添加到给定的 repaymentChainId
和 l1Token
的当前值。
对于每个 repaymentChainId
和 l1Token
组合,应查询较旧的
RootBundleExecuted 事件
以找到先前的 RootBundleExecuted
事件。这意味着识别 chainId
匹配 repaymentChainId
并且 identifying the runningBalanceValue
at the index of the l1Token
的最新 RootBundleExecuted
事件。
对于每个 l1Token
和 repaymentChainId
的元组,我们应该计算出一个总的运行余额值。以下算法描述了计算运行余额和净发送金额的过程:
spoke_balance_threshold = 此代币在 `spokeTargetBalances` 中的 "threshold"(阈值)值
spoke_balance_target = 此代币在 `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。
现在我们有了 ordered(排序)的 leaf,我们可以从 0 开始为每个 leaf 分配一个唯一的 leafId
。
有了所有这些信息,应该可以以格式化形式构造每个 leaf
here。
重要的是,l1Tokens
、bundleLpFees
、netSendAmounts
和 runningBalances
数组应具有相同的长度。后三个数组是映射到相同索引的 l1Tokens
条目的值。请参阅 此部分 以更好地解释如何将 l1Tokens
映射到其他三个数组。
构造 leaf 后,可以通过使用
Solidity 的标准流程 keccak256(abi.encode(poolRebalanceLeaf))
哈refundAmounts
和 refundAddresses
只是通过将这个群组中的 relays 按照 relayer
分组,并将每个 relay 的 amount - (amount * lpFeePct / 1e18)
相加计算得出。这些应该按照 refundAmounts
降序排列。如果两个 refundAmounts
相等,那么它们应该按照 relayer
地址排序。
如果对于一个特定的 l2TokenAddress
有超过 MAX_RELAYER_REPAYMENT_LEAF_SIZE
个 refundAddresses
,那么这些应该被分割成 MAX_RELAYER_REPAYMENT_LEAF_SIZE
个元素的叶子节点(按照上述方式排序),只有对于一个特定的 l2TokenAddress
的第一个叶子节点能够包含非零的 amountToReturn。
一旦这些为所有的 relays 计算出来,这些叶子节点(或者对于 > 25 个元素的叶子节点组)应该按照 chainId
作为主索引,然后 l2TokenAddress
作为辅助索引,以及 > MAX_RELAYER_REPAYMENT_LEAF_SIZE
个元素组的单独排序作为第三级排序。一旦这些被排序好,每个叶子节点可以基于其在群组中的索引被赋予一个 leafId
,从 0 开始。
一旦这些叶子节点被构造完成,它们可以被用于形成一个 merkle 根,如前一节所述。
要构建如 这里 所述的 SlowRelayRoot 叶子节点,只需基于在上面 "寻找慢速 Relays" 章节中找到的所有慢速 relays 形成叶子节点。 relays 中的信息应该直接映射到叶子节点数据结构。
它们的主排序索引应该是 originChainId
,辅助排序索引应该是 depositId
。
然后你可以构造一个 merkle 根,类似于在前两个章节中完成的方式。
提案被认为是有效的,必须满足三个条件:
bundleEvaluationBlockNumbers
必须包括在提案时具有非零
CrossChainContractsSet
的所有 chainIds
。如果提案被认为是无效的,返回 0。如果有效,返回 1。注意:这些值按 1e18
缩放。
- 原文链接: github.com/UMAprotocol/U...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!