这篇文章上次修改于 390 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

Arbitrum Nitro 的 native token 是 ETH,如何换成 ERC-20 token 呢?

Arbitrum Nitro 有两种模式,一种是 Rollup,一种是 AnyTrust,其中,Rollup 是需要将数据提交到父链的,而 AnyTrust 只需要把数据的证书提交到父链就可以了。

一笔交易的 gas fee 包含 L1 gas fee 和 L2 gas fee 两部分,对于 Rollup 来说,L1 gas fee 占比较高,约 97%,而 AnyTrust 的 L1 gas fee 则占比很小,因为它不需要将数据提交到 L1,所以 AnyTrust 的 L1 gas fee 估算不准确也可以接受,这样 AnyTrust 只需要改下配置文件就能直接更换 native token,详见 customize-deployment-configuration#gas-token

AnyTrust 是如何替换 native token 的

经过验证后,发现调用 getBalance() 查出来的是 ERC-20 token 的余额,MetaMask 展示的也是 ERC-20 token 余额,不再是 ETH 的余额了。也就是说,所有原来表示 ETH 余额的地方都统一换成了 ERC-20 token 余额,不需要更改 Arbitrum Nitro 代码。

Rollup 要如何替换 native token

部署界面 会生成 orbitSetupScriptConfig.json,里面有一个字段是 nativeToken,一个直接的想法把这个字段直接替换成 ERC-20 token,但是后来发现在执行 yarn run setup 时 deposit 不会成功。可见部署界面在部署 Rollup 和 AnyTrust 做了不同的事情。阅读源码后,发现它主要调用 rollupCreator.createRollup() 在父链上部署一些合约,也可以从 example 中发现 Rollup 和 AnyTrust 的区别是在调用该函数时是否传入 value(即 ETH 金额),Rollup 有 value,AnyTrust 用了 ERC-20 token 的话没有 value。Rollup 会在 L1 部署 Inbox、Bridge 等合约,AnyTrust 用了 ERC-20 token 的话会在 L1 部署 ERC20Inbox、ERC20Bridge 等合约。

接下来就是手动修改手动修改部署界面源码中的 native token 地址,需要根据错误提示注释掉 node_modules/@arbitrum/orbit-sdk/dist/createRollup.js 和 createRollupPrepareTransactionRequest.js 中的 Custom fee token can only be used on AnyTrust chains 异常,删掉 .next 文件夹后重新编译。启动后,按照提示执行,利用其生成的配置文件部署了 Orbit 链后,经验证,getBalance() 查出来的是 ERC-20 token 的余额,MetaMask 展示的也是 ERC-20 token 余额,达到了和 AnyTrust 一样的效果。

gas price 如何计算

还有一个问题,L1 gas fee 需要较为准确地算出,以支付给 poster,否则会导致 poster 收支不平衡,这就需要 ERC-20 token 在作为 gas token 时的值应较为准确,即应较为准确地得到 ERC-20 token 和 ETH 的换算关系。L1 gas fee 换算在 L2 上为 L1_gas_fee / L2_basefee,Arbitrum Nitro 中,gas price 是等于 l2 basefee 的,可以看下 l2 basefee 的算法,在白皮书第 3.3.1 章有

$$ F(B) = F_0 \cdot e^{max(0,β (B−B_0 ))} $$

其中,$F(B)$ 是 l2 basefee,$F_0$ 是 l2 minimum basefee,其它的符号可以忽略,大致上是用来反应网络拥堵情况的。可以看出 minimum basefee 对 basefee 的影响是最大的。

会发现部署界面生成的 orbitSetupScriptConfig.json 中有一个字段是 minL2BaseFee,所以如果只想较为粗糙地得到两者的换算关系,只需要修改该字段就可以了。

如果想较为准确地计算,可以在 nitro 的 StartTxHook() -> ApplyInternalTxUpdate() -> UpdatePricingModel()

// UpdatePricingModel updates the pricing model with info from the last block
func (ps *L2PricingState) UpdatePricingModel(l2BaseFee *big.Int, timePassed uint64, debug bool) {
    speedLimit, _ := ps.SpeedLimitPerSecond()
    _ = ps.AddToGasPool(int64(timePassed * speedLimit))
    inertia, _ := ps.PricingInertia()
    tolerance, _ := ps.BacklogTolerance()
    backlog, _ := ps.GasBacklog()
    minBaseFee, _ := ps.MinBaseFeeWei()
    baseFee := minBaseFee
    if backlog > tolerance*speedLimit {
        excess := int64(backlog - tolerance*speedLimit)
        exponentBips := arbmath.NaturalToBips(excess) / arbmath.Bips(inertia*speedLimit)
        baseFee = arbmath.BigMulByBips(minBaseFee, arbmath.ApproxExpBasisPoints(exponentBips))
    }
    _ = ps.SetBaseFeeWei(baseFee)
}

通过引入 Oracle 进行换算,该函数会在每个 block 开始时被调用。