Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Across 跨链桥合约解析 #158

Open
qiwihui opened this issue Mar 18, 2022 · 0 comments
Open

Across 跨链桥合约解析 #158

qiwihui opened this issue Mar 18, 2022 · 0 comments
Labels
区块链 区块链相关

Comments

@qiwihui
Copy link
Owner

qiwihui commented Mar 18, 2022

什么是 Across

以太坊跨链协议 Across 是一种新颖的跨链方法,它结合了乐观预言机(Optimistic Oracle)、绑定中继者和单边流动性池,可以提供从 Rollup 链到以太坊主网的去中心化即时交易。目前,Across 协议通过集成以太坊二层扩容方案Optimism、Arbitrum和Boba Network支持双向桥接,即可将资产从L1发送至L2,亦可从L2发送至L1。

存款跨链流程

process

来源于:https://docs.across.to/bridge/how-does-across-work-1/architecture-process-walkthrough

Across 协议中,存款跨链有几种可能的流程,最重要的是,存款人在任何这些情况下都不会损失资金。在每一种情况下,在 L2 上存入的任何代币都会通过 Optimism 或 Arbitrum 的原生桥转移到 L1 上的流动池,用以偿还给流动性提供者。

从上面的流程中,我们可以看到 Across 协议流程包括以下几种:

  • 即时中继,无争议;
  • 即时中继,有争议;
  • 慢速中继,无争议;
  • 慢速中继,有争议;
  • 慢速中继,加速为即时中继。

Across 协议中主要包括几类角色:

  • 存款者(Depositor):需要将资产从二层链转移到L1的用户;
  • 中继者(Relayer):负责将L1层资产转移给用户,以及L2层资产跨链的节点;
  • 流动性提供者(LP):为流动性池提供资产;
  • 争议者(Disputor):对中继过程有争议的人,可以向 Optimistic Oracle 提交争议;

项目总览

Across 的合约源码地址为 https://github.com/across-protocol/contracts-v1,目前 Across Protocol 正在进行 v2 版本合约的开发,我们这一篇文章主要分析 v1 版本的合约源码。首先我们下载源码:

git clone https://github.com/across-protocol/contracts-v1
cd contracts-v1

合约源码的主要的目录结构为:

contract-v1
├── contracts // Across protocol 的合约源码
├── deploy // 部署脚本
├── hardhat.config.js // hardhat 配置
├── helpers // 辅助函数
├── networks // 合约在不同链上的部署地址
└── package.json // 依赖包

在这篇解析中,我们主要关注 contractsdeploy 目录下的文件。

合约总览

合约目录 contracts 的目录结构为:

contracts/
├── common
│   ├── implementation
│   └── interfaces
├── external
│   ├── README.md
│   ├── avm
│   ├── chainbridge
│   ├── ovm
│   └── polygon
├── insured-bridge
│   ├── BridgeAdmin.sol
│   ├── BridgeDepositBox.sol
│   ├── BridgePool.sol
│   ├── RateModelStore.sol
│   ├── avm
│   ├── interfaces
│   ├── ovm
│   └── test
└── oracle
    ├── implementation
    └── interfaces

其中,各个目录包含的内容为:

  • common:一些通用功能的库方法等,包括:
  • external:外部合约,主要用于实现在管理员合约中对不同 L2 的消息发送;
  • insured-bridge 合约主要功能,我们会在接下来的章节章节中重点分析;
  • oracle:主要是 Optimistic Oracle 提供功能的方法接口,在这篇文章中我们不对 Optimistic Oracle 的原理实现进行介绍,主要会介绍 Across 协议会在何处使用 Optimistic Oracle。

接下来我们会重点分析 insured-bridge 中的合约的功能,这是 Across 主要功能的合约所在。

insured-bridge 目录中:

  • BridgeAdmin.sol :管理合约,负责管理和生成生成 L2 上的 DepositBox 合约和 L1 上的 BridgePool 合约;
  • BridgeDepositBox.sol :L2 层上负责存款的抽象合约,Arbitrum,Optimism 和 Boba 网络的合约都是继承自这个合约;
  • BridgePool.sol :桥接池合约,管理 L1 层资金池。

BridgeAdmin

这个合约是管理员合约,部署在L1层,并有权限管理 L1 层上的流动性池和 L2 上的存款箱(DepositBoxes)。可以注意的是,这个合约的管理帐号是一个多钱钱包,避免了一些安全问题。

首先我们看到合约中的几个状态变量:

contract BridgeAdmin is BridgeAdminInterface, Ownable, Lockable {

    address public override finder;

    mapping(uint256 => DepositUtilityContracts) private _depositContracts;

    mapping(address => L1TokenRelationships) private _whitelistedTokens;

    // Set upon construction and can be reset by Owner.
    uint32 public override optimisticOracleLiveness;
    uint64 public override proposerBondPct;
    bytes32 public override identifier;

    constructor(
        address _finder,
        uint32 _optimisticOracleLiveness,
        uint64 _proposerBondPct,
        bytes32 _identifier
    ) {
        finder = _finder;
        require(address(_getCollateralWhitelist()) != address(0), "Invalid finder");
        _setOptimisticOracleLiveness(_optimisticOracleLiveness);
        _setProposerBondPct(_proposerBondPct);
        _setIdentifier(_identifier);
    }

...

其中:

  • finder 用来记录查询最新 OptimisticOracle 和 UMA 生态中其他合约的合约地址;
  • _depositContracts 该合约可以将消息中继到任意数量的 L2 存款箱,每个 L2 网络一个,每个都由唯一的网络 ID 标识。 要中继消息,需要存储存款箱合约地址和信使(messenger)合约地址。 每个 L2 的信使实现不同,因为 L1 --> L2 消息传递是非标准的;
  • _whitelistedTokens 记录了 L1 代币地址与对应 L2 代币地址以及桥接池的映射;
  • optimisticOracleLiveness 中继存款的争议时长;
  • proposerBondPct Optimistic Oracle 中 proposer 的绑定费率

管理员可以设置以上这些变量的内容,以及可以设置每秒的 LP 费率,转移桥接池的管理员权限等。

同时,管理员还可以通过信使设置 L2 层合约的参数,包括;

  • setCrossDomainAdmin :设置 L2 存款合约的管理员地址;
  • setMinimumBridgingDelay :设置 L2 存款合约的最小桥接延迟;
  • setEnableDepositsAndRelays:开启或者暂停代币 L2 存款,这个方法会同时暂停 L1 层桥接池;
  • whitelistToken:关联 L2 代币地址,这样这个代币就可以开始存款和中继;

对于消息发送,管理员合约通过调用不同的信使的 relayMessage 方法来完成,将 msg.value == l1CallValue 发送给信使,然后它可以以任何方式使用它来执行跨域消息。

    function _relayMessage(
        address messengerContract,
        uint256 l1CallValue,
        address target,
        address user,
        uint256 l2Gas,
        uint256 l2GasPrice,
        uint256 maxSubmissionCost,
        bytes memory message
    ) private {
        require(l1CallValue == msg.value, "Wrong number of ETH sent");
        MessengerInterface(messengerContract).relayMessage{ value: l1CallValue }(
            target,
            user,
            l1CallValue,
            l2Gas,
            l2GasPrice,
            maxSubmissionCost,
            message
        );
    }

不同L2的消息方法分别在对应链的 CrossDomainEnabled.sol 合约中,比如:

  • Arbitrum: contracts/insured-bridge/avm/Arbitrum_CrossDomainEnabled.sol
  • Optimism,Boba: contracts/insured-bridge/ovm/OVM_CrossDomainEnabled.sol

BridgeDepositBox

接下来我们看到 BridgeDepositBox.sol,抽象合约 BridgeDepositBox 合约中主要有两个功能。

bridgeTokens

第一个是 bridgeTokens 方法,用于将 L2 层代币通过原生代币桥转移到 L1 上,这个方法需要在不同的 L2 层合约上实现,目前支持的 L2 层包括 Arbitrum,Optimism 和 Boba,分别对应的文件为:

  • Arbitrum: contracts/insured-bridge/avm/AVM_BridgeDepositBox.sol
  • Optimism: contracts/insured-bridge/ovm/OVM_BridgeDepositBox.sol
  • Boba: contracts/insured-bridge/ovm/OVM_OETH_BridgeDepositBox.sol

以 Arbitrum 链上的 bridgeToken 为例:

    // BridgeDepositBox.sol 文件中
    function canBridge(address l2Token) public view returns (bool) {
        return isWhitelistToken(l2Token) && _hasEnoughTimeElapsedToBridge(l2Token);
    }

		// AVM_BridgeDepositBox.sol文件中
    function bridgeTokens(address l2Token, uint32 l1Gas) public override nonReentrant() {
        uint256 bridgeDepositBoxBalance = TokenLike(l2Token).balanceOf(address(this));
        require(bridgeDepositBoxBalance > 0, "can't bridge zero tokens");
        require(canBridge(l2Token), "non-whitelisted token or last bridge too recent");

        whitelistedTokens[l2Token].lastBridgeTime = uint64(getCurrentTime());

        StandardBridgeLike(l2GatewayRouter).outboundTransfer(
            whitelistedTokens[l2Token].l1Token, // _l1Token. Address of the L1 token to bridge over.
            whitelistedTokens[l2Token].l1BridgePool, // _to. Withdraw, over the bridge, to the l1 withdraw contract.
            bridgeDepositBoxBalance, // _amount. Send the full balance of the deposit box to bridge.
            "" // _data. We don't need to send any data for the bridging action.
        );

        emit TokensBridged(l2Token, bridgeDepositBoxBalance, l1Gas, msg.sender);
    }

bridgeTokens 上有一个装饰器 canBridge 包含两个判断, isWhitelistToken 用于判断对应 L2 层代币是否已经在 L1 层上添加了桥接池, _hasEnoughTimeElapsedToBridge 用来减少频繁跨连导致的费用消耗问题,因此设置了最小的跨链接时间。

bridgeTokens 主要就是调用了 L2 层原生的跨链方法,比如 outboundTransfer

deposit

第二个是 deposit 方法用于将 L2 层资产转移到以太坊 L1 层上,对应与前端页面 Deposit 操作。对应代码为:

    function bridgeTokens(address l2Token, uint32 l2Gas) public virtual;

    function deposit(
        address l1Recipient,
        address l2Token,
        uint256 amount,
        uint64 slowRelayFeePct,
        uint64 instantRelayFeePct,
        uint64 quoteTimestamp
    ) public payable onlyIfDepositsEnabled(l2Token) nonReentrant() {
        require(isWhitelistToken(l2Token), "deposit token not whitelisted");

        require(slowRelayFeePct <= 0.25e18, "slowRelayFeePct must be <= 25%");
        require(instantRelayFeePct <= 0.25e18, "instantRelayFeePct must be <= 25%");

        require(
            getCurrentTime() >= quoteTimestamp - 10 minutes && getCurrentTime() <= quoteTimestamp + 10 minutes,
            "deposit mined after deadline"
        );
        
        if (whitelistedTokens[l2Token].l1Token == l1Weth && msg.value > 0) {
            require(msg.value == amount, "msg.value must match amount");
            WETH9Like(address(l2Token)).deposit{ value: msg.value }();
        }
        else IERC20(l2Token).safeTransferFrom(msg.sender, address(this), amount);

        emit FundsDeposited(
            chainId,
            numberOfDeposits, // depositId: the current number of deposits acts as a deposit ID (nonce).
            l1Recipient,
            msg.sender,
            whitelistedTokens[l2Token].l1Token,
            l2Token,
            amount,
            slowRelayFeePct,
            instantRelayFeePct,
            quoteTimestamp
        );

        numberOfDeposits += 1;
    }

其中,合约区分了 ETH 和 ERC20 代币的存入方式。

存入资产后,合约产生了一个事件 FundsDeposited,用于中继者程序捕获并进行资产跨链,事件信息包含合约部署的 L2 链ID,存款ID numberOfDeposits,L1层接收者,存款者,L1和L2层代币地址,数量和费率,以及时间戳。

BridgePool

BridgePool 合约部署在 Layer 1 上,提供了给中继者完成 Layer2 上存款订单的函数。主要包含以下功能:

  1. 流动性提供者添加和删除流动性的方法 addLiquidityremoveLiquidity
  2. 慢速中继: relayDeposit
  3. 即时中继: relayAndSpeedUpspeedUpRelay
  4. 争议: disputeRelay
  5. 解决中继: settleRelay

构造器

在合约初始时,合约设置了对应的桥管理员地址,L1代币地址,每秒的 LP 费率,以及标识是否为 WETH 池。同时,通过 syncUmaEcosystemParamssyncWithBridgeAdminParams 两个方法同步了 Optimistic Oracle 地址信息,Store 的地址信息,以及对应的 ProposerBondPctOptimisticOracleLiveness 等参数。

    function syncUmaEcosystemParams() public nonReentrant() {
        FinderInterface finder = FinderInterface(bridgeAdmin.finder());
        optimisticOracle = SkinnyOptimisticOracleInterface(
            finder.getImplementationAddress(OracleInterfaces.SkinnyOptimisticOracle)
        );

        store = StoreInterface(finder.getImplementationAddress(OracleInterfaces.Store));
        l1TokenFinalFee = store.computeFinalFee(address(l1Token)).rawValue;
    }

		function syncWithBridgeAdminParams() public nonReentrant() {
        proposerBondPct = bridgeAdmin.proposerBondPct();
        optimisticOracleLiveness = bridgeAdmin.optimisticOracleLiveness();
        identifier = bridgeAdmin.identifier();
    }

		constructor(
        string memory _lpTokenName,
        string memory _lpTokenSymbol,
        address _bridgeAdmin,
        address _l1Token,
        uint64 _lpFeeRatePerSecond,
        bool _isWethPool,
        address _timer
    ) Testable(_timer) ERC20(_lpTokenName, _lpTokenSymbol) {
        require(bytes(_lpTokenName).length != 0 && bytes(_lpTokenSymbol).length != 0, "Bad LP token name or symbol");
        bridgeAdmin = BridgeAdminInterface(_bridgeAdmin);
        l1Token = IERC20(_l1Token);
        lastLpFeeUpdate = uint32(getCurrentTime());
        lpFeeRatePerSecond = _lpFeeRatePerSecond;
        isWethPool = _isWethPool;

        syncUmaEcosystemParams(); // Fetch OptimisticOracle and Store addresses and L1Token finalFee.
        syncWithBridgeAdminParams(); // Fetch ProposerBondPct OptimisticOracleLiveness, Identifier from the BridgeAdmin.

        emit LpFeeRateSet(lpFeeRatePerSecond);
    }

添加和删除流动性

我们首先看到添加和删除流动性,添加流动性即流动性提供者向连接池中提供 L1 代币,并获取相应数量的 LP 代币作为证明,LP 代币数量根据现行汇率计算。

    function addLiquidity(uint256 l1TokenAmount) public payable nonReentrant() {
				// 如果是 weth 池,调用发送 msg.value,msg.value 与 l1TokenAmount 相同
				// 否则,msg.value 必需为 0
        require((isWethPool && msg.value == l1TokenAmount) || msg.value == 0, "Bad add liquidity Eth value");

			  // 由于 `_exchangeRateCurrent()` 读取合约的余额并使用它更新合约状态,
				// 因此我们必需在转入任何代币之前调用
        uint256 lpTokensToMint = (l1TokenAmount * 1e18) / _exchangeRateCurrent();
        _mint(msg.sender, lpTokensToMint);
        liquidReserves += l1TokenAmount;

        if (msg.value > 0 && isWethPool) WETH9Like(address(l1Token)).deposit{ value: msg.value }();
        else l1Token.safeTransferFrom(msg.sender, address(this), l1TokenAmount);

        emit LiquidityAdded(l1TokenAmount, lpTokensToMint, msg.sender);
    }

由于合约支持 WETH 作为流动性池,因此添加流动性区分了 WETH 和其他 ERC20 代币的添加方法。

此处的难点在于 LP 代币和 L1 代币之间的汇率换算 _exchangeRateCurrent 的实现,我们从合约中提取出了 _exchangeRateCurrent 所使用的函数,包括 _updateAccumulatedLpFees_sync

	
		function _getAccumulatedFees() internal view returns (uint256) {
        uint256 possibleUnpaidFees =
            (undistributedLpFees * lpFeeRatePerSecond * (getCurrentTime() - lastLpFeeUpdate)) / (1e18);
        return possibleUnpaidFees < undistributedLpFees ? possibleUnpaidFees : undistributedLpFees;
    }

    function _updateAccumulatedLpFees() internal {
        uint256 unallocatedAccumulatedFees = _getAccumulatedFees();

        undistributedLpFees = undistributedLpFees - unallocatedAccumulatedFees;

        lastLpFeeUpdate = uint32(getCurrentTime());
    }

		function _sync() internal {
        uint256 l1TokenBalance = l1Token.balanceOf(address(this)) - bonds;
        if (l1TokenBalance > liquidReserves) {
            
            utilizedReserves -= int256(l1TokenBalance - liquidReserves);
            liquidReserves = l1TokenBalance;
        }
    }
    
		function _exchangeRateCurrent() internal returns (uint256) {
        if (totalSupply() == 0) return 1e18; // initial rate is 1 pre any mint action.

        _updateAccumulatedLpFees();
        _sync();

        int256 numerator = int256(liquidReserves) + utilizedReserves - int256(undistributedLpFees);
        return (uint256(numerator) * 1e18) / totalSupply();
    }

换算汇率等于当前合约中代币的储备与总 LP 供应量的比值,计算步骤如下:

  1. 更新自上次方法调用以来的累积LP费用 _updateAccumulatedLpFees
    1. 计算可能未付的费用 possibleUnpaidFees ,等于未分配的 Lp 费用 undistributedLpFees * 每秒 LP 费率 *(当前时间-上次更新时间),目前 WETH 桥接池中每秒LP费率为 0.0000015。
    2. 计算累积费用 unallocatedAccumulatedFees ,如果 possibleUnpaidFees 小于未分配的 Lp 费用,则所有未分配的 LP 费用都将用于累积费用;
    3. 当前未分配 LP 费用 = 原先未分配 LP 费用 - 累积费用;
  2. 计算由于代币桥接产生的余额变化
    1. 当前合约中的代币储备=当前合约中的代币数量 - 被绑定在中继过程中的代币数量;
    2. 如果当前合约中的代币储备大于流动储备 liquidReserves,则被使用的储备 utilizedReserves = 原先被使用的储备 -(当前合约中的代币储备 - 流动储备);
    3. 当前流动性储备 = 当前合约中的代币储备;
  3. 计算汇率:
    1. 经过更新之后,汇率计算的分子:流动储备 + 被使用的储备 - 未被分配 LP 费用;
    2. 分子与LP 代币总供应量的比值即为换算汇率。

利用换算汇率,可以计算得到添加 l1TokenAmount 数量的代币时所能得到的 LP 代币的数量。

对于移除流动性,过程与添加流动性相反,这里不再赘述。

    function removeLiquidity(uint256 lpTokenAmount, bool sendEth) public nonReentrant() {
        // 如果是 WETH 池,则只能通过发送 ETH 来取出流动性
        require(!sendEth || isWethPool, "Cant send eth");
        uint256 l1TokensToReturn = (lpTokenAmount * _exchangeRateCurrent()) / 1e18;

        // 检查是否有足够的流储备来支持取款金额
        require(liquidReserves >= (pendingReserves + l1TokensToReturn), "Utilization too high to remove");

        _burn(msg.sender, lpTokenAmount);
        liquidReserves -= l1TokensToReturn;

        if (sendEth) _unwrapWETHTo(payable(msg.sender), l1TokensToReturn);
        else l1Token.safeTransfer(msg.sender, l1TokensToReturn);

        emit LiquidityRemoved(l1TokensToReturn, lpTokenAmount, msg.sender);
    }

慢速中继

慢速中继,以及之后要讨论的即时中继,都会用到 DepositDataRelayData 这两个数据,前者表示存框交易的数据,后者表示中继交易的信息。

		// 来自 L2 存款交易的数据。
    struct DepositData {
        uint256 chainId;
        uint64 depositId;
        address payable l1Recipient;
        address l2Sender;
        uint256 amount;
        uint64 slowRelayFeePct;
        uint64 instantRelayFeePct;
        uint32 quoteTimestamp;
    }

		// 每个 L2 存款在任何时候都可以进行一次中继尝试。 中继尝试的特征在于其 RelayData。
    struct RelayData {
        RelayState relayState;
        address slowRelayer;
        uint32 relayId;
        uint64 realizedLpFeePct;
        uint32 priceRequestTime;
        uint256 proposerBond;
        uint256 finalFee;
    }

下面我们看到 relayDeposit 方法,这个方法由中继者调用,执行从 L2 到 L1 的慢速中继。对于每一个存款而言,只能有一个待处理的中继,这个待处理的中继不包括有争议的中继。

    function relayDeposit(DepositData memory depositData, uint64 realizedLpFeePct)
        public
        onlyIfRelaysEnabld()
        nonReentrant()
    {
				// realizedLPFeePct 不超过 50%,慢速和即时中继费用不超过25%,费用合计不超过100%
        require(
            depositData.slowRelayFeePct <= 0.25e18 &&
                depositData.instantRelayFeePct <= 0.25e18 &&
                realizedLpFeePct <= 0.5e18,
            "Invalid fees"
        );

        // 查看是否已经有待处理的中继
        bytes32 depositHash = _getDepositHash(depositData);

				// 对于有争议的中继,relays 中对应的 hash 会被删除,这个条件可以通过
        require(relays[depositHash] == bytes32(0), "Pending relay exists");

				// 如果存款没有正在执行的中继,则关联调用者的中继尝试
        uint32 priceRequestTime = uint32(getCurrentTime());

        uint256 proposerBond = _getProposerBond(depositData.amount);

        // 保存新中继尝试参数的哈希值。
        // 注意:这个中继的活跃时间(liveness)可以在 BridgeAdmin 中更改,这意味着每个中继都有一个潜在的可变活跃时间。
				// 这不应该提供任何被利用机会,特别是因为 BridgeAdmin 状态(包括 liveness 值)被许可给跨域所有者。
				RelayData memory relayData =
            RelayData({
                relayState: RelayState.Pending,
                slowRelayer: msg.sender,
                relayId: numberOfRelays++, // 注意:在将 relayId 设置为其当前值的同时增加 numberOfRelays。
                realizedLpFeePct: realizedLpFeePct,
                priceRequestTime: priceRequestTime,
                proposerBond: proposerBond,
                finalFee: l1TokenFinalFee
            });
        relays[depositHash] = _getRelayDataHash(relayData);

        bytes32 relayHash = _getRelayHash(depositData, relayData);

				// 健全性检查池是否有足够的余额来支付中继金额 + 提议者奖励。 OptimisticOracle 价格请求经过挑战期后,将在结算时支付奖励金额。
        // 注意:liquidReserves 应该总是 <= balance - bonds。
        require(liquidReserves - pendingReserves >= depositData.amount, "Insufficient pool balance");

				// 计算总提议保证金并从调用者那里拉取,以便 OptimisticOracle 可以从这里拉取它。
        uint256 totalBond = proposerBond + l1TokenFinalFee;
        pendingReserves += depositData.amount; // 在正在处理的准备中预订此中继使用的最大流动性。
        bonds += totalBond;

        l1Token.safeTransferFrom(msg.sender, address(this), totalBond);
        emit DepositRelayed(depositHash, depositData, relayData, relayHash);
    }

可以看到,存款哈希与 depositData 有关,中继哈希与 depositDatarelayData 都有关。最后我们可以看到, relayDeposit 还未实际付款给用户的 L1 地址,需要等待中继者处理,或者通过加速处理中继。

加速中继

speedUpRelay 方法立即将存款金额减去费用后转发给 l1Recipient,即时中继者在待处理的中继挑战期后获得奖励。

    // 我们假设调用者已经执行了链外检查,以确保他们尝试中继的存款数据是有效的。
		// 如果存款数据无效,则即时中继者在无效存款数据发生争议后无权收回其资金。
		// 此外,没有人能够重新提交无效存款数据的中继,因为他们知道这将再次引起争议。
		// 另一方面,如果存款数据是有效的,那么即使它被错误地争议,即时中继者最终也会得到补偿,
		// 因为会激励其他人重新提交中继,以获得慢中继者的奖励。
		// 一旦有效中继最终确定,即时中继将得到补偿。因此,调用者在验证中继数据方面与争议者具有相同的责任。
		function speedUpRelay(DepositData memory depositData, RelayData memory relayData) public nonReentrant() {
        bytes32 depositHash = _getDepositHash(depositData);
        _validateRelayDataHash(depositHash, relayData);
        bytes32 instantRelayHash = _getInstantRelayHash(depositHash, relayData);
        require(
            // 只能在没有与之关联的现有即时中继的情况下加速待处理的中继。
            getCurrentTime() < relayData.priceRequestTime + optimisticOracleLiveness &&
                relayData.relayState == RelayState.Pending &&
                instantRelays[instantRelayHash] == address(0),
            "Relay cannot be sped up"
        );
        instantRelays[instantRelayHash] = msg.sender;

        // 从调用者那里提取中继金额减去费用并发送存款到 l1Recipient。
				// 支付的总费用是 LP 费用、中继费用和即时中继费用的总和。
        uint256 feesTotal =
            _getAmountFromPct(
                relayData.realizedLpFeePct + depositData.slowRelayFeePct + depositData.instantRelayFeePct,
                depositData.amount
            );
        // 如果 L1 代币是 WETH,那么:a) 从即时中继者提取 WETH b) 解包 WETH 为 ETH c) 将 ETH 发送给接收者。
        uint256 recipientAmount = depositData.amount - feesTotal;
        if (isWethPool) {
            l1Token.safeTransferFrom(msg.sender, address(this), recipientAmount);
            _unwrapWETHTo(depositData.l1Recipient, recipientAmount);
            // 否则,这是一个普通的 ERC20 代币。 发送给收件人。
        } else l1Token.safeTransferFrom(msg.sender, depositData.l1Recipient, recipientAmount);

        emit RelaySpedUp(depositHash, msg.sender, relayData);
    }

即时中继

relayAndSpeedUp 执行即时中继。这个方法的函数内容与 relayDepositspeedUpRelay 方法是一致的,这里就不具体注释了,可以参考前文中的注释。这个函数的代码几乎是直接将 relayDepositspeedUpRelay 的代码进行了合并,代码冗余。

    // 由 Relayer 调用以执行从 L2 到 L1 的慢 + 快中继,完成相应的存款订单。
    // 存款只能有一个待处理的中继。此方法实际上是串联的 relayDeposit 和 speedUpRelay 方法。
		// 这可以重构为只调用每个方法,但是结合传输和哈希计算可以节省一些 gas。
		function relayAndSpeedUp(DepositData memory depositData, uint64 realizedLpFeePct)
        public
        onlyIfRelaysEnabld()
        nonReentrant()
    {
        uint32 priceRequestTime = uint32(getCurrentTime());

        require(
            depositData.slowRelayFeePct <= 0.25e18 &&
                depositData.instantRelayFeePct <= 0.25e18 &&
                realizedLpFeePct <= 0.5e18,
            "Invalid fees"
        );

        bytes32 depositHash = _getDepositHash(depositData);

        require(relays[depositHash] == bytes32(0), "Pending relay exists");

        uint256 proposerBond = _getProposerBond(depositData.amount);

        RelayData memory relayData =
            RelayData({
                relayState: RelayState.Pending,
                slowRelayer: msg.sender,
                relayId: numberOfRelays++, // Note: Increment numberOfRelays at the same time as setting relayId to its current value.
                realizedLpFeePct: realizedLpFeePct,
                priceRequestTime: priceRequestTime,
                proposerBond: proposerBond,
                finalFee: l1TokenFinalFee
            });
        bytes32 relayHash = _getRelayHash(depositData, relayData);
        relays[depositHash] = _getRelayDataHash(relayData);

        bytes32 instantRelayHash = _getInstantRelayHash(depositHash, relayData);
        require(
            instantRelays[instantRelayHash] == address(0),
            "Relay cannot be sped up"
        );

        require(liquidReserves - pendingReserves >= depositData.amount, "Insufficient pool balance");

        uint256 totalBond = proposerBond + l1TokenFinalFee;

        uint256 feesTotal =
            _getAmountFromPct(
                relayData.realizedLpFeePct + depositData.slowRelayFeePct + depositData.instantRelayFeePct,
                depositData.amount
            );
        uint256 recipientAmount = depositData.amount - feesTotal;

        bonds += totalBond;
        pendingReserves += depositData.amount;

        instantRelays[instantRelayHash] = msg.sender;

        l1Token.safeTransferFrom(msg.sender, address(this), recipientAmount + totalBond);

        if (isWethPool) {
            _unwrapWETHTo(depositData.l1Recipient, recipientAmount);
        } else l1Token.safeTransfer(depositData.l1Recipient, recipientAmount);

        emit DepositRelayed(depositHash, depositData, relayData, relayHash);
        emit RelaySpedUp(depositHash, msg.sender, relayData);
    }

争议

当对待处理的中继提出争议时,争议者需要想 Optimistic Oracle 提交提案,并等待争议解决。

    // 由 Disputer 调用以对待处理的中继提出争议。
		// 这个方法的结果是总是抛出中继,为另一个中继者提供处理相同存款的机会。
		// 在争议者和提议者之间,谁不正确,谁就失去了他们的质押。谁是正确的,谁就拿回来并获得一笔钱。
		function disputeRelay(DepositData memory depositData, RelayData memory relayData) public nonReentrant() {
        require(relayData.priceRequestTime + optimisticOracleLiveness > getCurrentTime(), "Past liveness");
        require(relayData.relayState == RelayState.Pending, "Not disputable");
        // 检验输入数据
        bytes32 depositHash = _getDepositHash(depositData);
        _validateRelayDataHash(depositHash, relayData);

        // 将提案和争议提交给 Optimistic Oracle。
        bytes32 relayHash = _getRelayHash(depositData, relayData);

        // 注意:在某些情况下,这会由于 Optimistic Oracle 的变化而失败,并且该方法将退还中继者。
        bool success =
            _requestProposeDispute(
                relayData.slowRelayer,
                msg.sender,
                relayData.proposerBond,
                relayData.finalFee,
                _getRelayAncillaryData(relayHash)
            );

				// 放弃中继并从跟踪的保证金中移除中继的保证金。
        bonds -= relayData.finalFee + relayData.proposerBond;
        pendingReserves -= depositData.amount;
        delete relays[depositHash];
        if (success) emit RelayDisputed(depositHash, _getRelayDataHash(relayData), msg.sender);
        else emit RelayCanceled(depositHash, _getRelayDataHash(relayData), msg.sender);
    }

其中, _requestProposeDispute 的函数内容如下:

    // 向 optimistic oracle 提议与 `customAncillaryData` 相关的中继事件的新价格为真。
		// 如果有人不同意中继参数,不管他们是否映射到 L2 存款,他们可以与预言机争议。
    function _requestProposeDispute(
        address proposer,
        address disputer,
        uint256 proposerBond,
        uint256 finalFee,
        bytes memory customAncillaryData
    ) private returns (bool) {
        uint256 totalBond = finalFee + proposerBond;
        l1Token.safeApprove(address(optimisticOracle), totalBond);
        try
            optimisticOracle.requestAndProposePriceFor(
                identifier,
                uint32(getCurrentTime()),
                customAncillaryData,
                IERC20(l1Token),
                // 将奖励设置为 0,因为在中继提案经过挑战期后,我们将直接从该合约中结算提案人奖励支出。
                0,
                // 为价格请求设置 Optimistic oracle 提议者保证金。
                proposerBond,
                // 为价格请求设置 Optimistic oracle 活跃时间。
                optimisticOracleLiveness,
                proposer,
                // 表示 "True"; 及提议的中继是合法的
                int256(1e18)
            )
        returns (uint256 bondSpent) {
            if (bondSpent < totalBond) {
                // 如果 Optimistic oracle 拉取得更少(由于最终费用的变化),则退还提议者。
                uint256 refund = totalBond - bondSpent;
                l1Token.safeTransfer(proposer, refund);
                l1Token.safeApprove(address(optimisticOracle), 0);
                totalBond = bondSpent;
            }
        } catch {
            // 如果 Optimistic oracle 中出现错误,这意味着已经更改了某些内容以使该请求无可争议。
						// 为确保请求不会默认通过,退款提议者并提前返回,允许调用方法删除请求,但 Optimistic oracle 没有额外的追索权。
            l1Token.safeTransfer(proposer, totalBond);
            l1Token.safeApprove(address(optimisticOracle), 0);

            // 提早返回,注意到提案+争议的尝试没有成功。
            return false;
        }

        SkinnyOptimisticOracleInterface.Request memory request =
            SkinnyOptimisticOracleInterface.Request({
                proposer: proposer,
                disputer: address(0),
                currency: IERC20(l1Token),
                settled: false,
                proposedPrice: int256(1e18),
                resolvedPrice: 0,
                expirationTime: getCurrentTime() + optimisticOracleLiveness,
                reward: 0,
                finalFee: totalBond - proposerBond,
                bond: proposerBond,
                customLiveness: uint256(optimisticOracleLiveness)
            });

        // 注意:在此之前不要提取资金,以避免任何不需要的转账。
        l1Token.safeTransferFrom(msg.sender, address(this), totalBond);
        l1Token.safeApprove(address(optimisticOracle), totalBond);
        // 对我们刚刚发送的请求提出争议。
        optimisticOracle.disputePriceFor(
            identifier,
            uint32(getCurrentTime()),
            customAncillaryData,
            request,
            disputer,
            address(this)
        );

        // 返回 true 表示提案 + 争议调用成功。
        return true;
    }

最后,我们来看看 settleRelay

    // 如果待处理中继价格请求在 OptimisticOracle 上有可用的价格,则奖励中继者,并将中继标记为完成。
	  // 我们使用 relayData 和 depositData 来计算中继价格请求在 OptimisticOracle 上唯一关联的辅助数据。
		// 如果传入的价格请求与待处理的中继价格请求不匹配,那么这将恢复(revert)。
		function settleRelay(DepositData memory depositData, RelayData memory relayData) public nonReentrant() {
        bytes32 depositHash = _getDepositHash(depositData);
        _validateRelayDataHash(depositHash, relayData);
        require(relayData.relayState == RelayState.Pending, "Already settled");
        uint32 expirationTime = relayData.priceRequestTime + optimisticOracleLiveness;
        require(expirationTime <= getCurrentTime(), "Not settleable yet");

        // 注意:此检查是为了给中继者一小段但合理的时间来完成中继,然后再被其他人“偷走”。
				// 这是为了确保有动力快速解决中继。
        require(
            msg.sender == relayData.slowRelayer || getCurrentTime() > expirationTime + 15 minutes,
            "Not slow relayer"
        );

        // 将中继状态更新为已完成。 这可以防止中继的任何重新设处理。
        relays[depositHash] = _getRelayDataHash(
            RelayData({
                relayState: RelayState.Finalized,
                slowRelayer: relayData.slowRelayer,
                relayId: relayData.relayId,
                realizedLpFeePct: relayData.realizedLpFeePct,
                priceRequestTime: relayData.priceRequestTime,
                proposerBond: relayData.proposerBond,
                finalFee: relayData.finalFee
            })
        );

        // 奖励中继者并支付 l1Recipient。
         // 此时有两种可能的情况:
         // - 这是一个慢速中继:在这种情况下,a) 向慢速中继者支付奖励 b) 向 l1Recipient 支付
         //   金额减去已实现的 LP 费用和慢速中继费用。 转账没有加快,所以没有即时费用。
         // - 这是一个即时中继:在这种情况下,a) 向慢速中继者支付奖励 b) 向即时中继者支付
         //   全部桥接金额,减去已实现的 LP 费用并减去慢速中继费用。
				//    当即时中继者调用 speedUpRelay 时,它们存入的金额相同,减去即时中继者费用。
				//    结果,他们实际上得到了加速中继时所花费的费用 + InstantRelayFee。

        uint256 instantRelayerOrRecipientAmount =
            depositData.amount -
                _getAmountFromPct(relayData.realizedLpFeePct + depositData.slowRelayFeePct, depositData.amount);

        // 如果即时中继参数与批准的中继相匹配,则退款给即时中继者。
        bytes32 instantRelayHash = _getInstantRelayHash(depositHash, relayData);
        address instantRelayer = instantRelays[instantRelayHash];

        // 如果这是 WETH 池并且即时中继者是地址 0x0(即中继没有加速),那么:
        // a) 将 WETH 提取到 ETH 和 b) 将 ETH 发送给接收者。
        if (isWethPool && instantRelayer == address(0)) {
            _unwrapWETHTo(depositData.l1Recipient, instantRelayerOrRecipientAmount);
            // 否则,这是一个正常的慢速中继正在完成,合约将 ERC20 发送给接收者,
						// 或者这是一个即时中继的最终完成,我们需要用 WETH 偿还即时中继者。
        } else
            l1Token.safeTransfer(
                instantRelayer != address(0) ? instantRelayer : depositData.l1Recipient,
                instantRelayerOrRecipientAmount
            );

        // 需要支付费用和保证金。费用归解决者。保证金总是归到慢速中继者。
        // 注意:为了 gas 效率,我们使用 `if`,所以如果它们是相同的地址,我们可以合并这些转账。
        uint256 slowRelayerReward = _getAmountFromPct(depositData.slowRelayFeePct, depositData.amount);
        uint256 totalBond = relayData.finalFee + relayData.proposerBond;
        if (relayData.slowRelayer == msg.sender)
            l1Token.safeTransfer(relayData.slowRelayer, slowRelayerReward + totalBond);
        else {
            l1Token.safeTransfer(relayData.slowRelayer, totalBond);
            l1Token.safeTransfer(msg.sender, slowRelayerReward);
        }

        uint256 totalReservesSent = instantRelayerOrRecipientAmount + slowRelayerReward;

        // 按更改的金额和分配的 LP 费用更新储备。
        pendingReserves -= depositData.amount;
        liquidReserves -= totalReservesSent;
        utilizedReserves += int256(totalReservesSent);
        bonds -= totalBond;
        _updateAccumulatedLpFees();
        _allocateLpFees(_getAmountFromPct(relayData.realizedLpFeePct, depositData.amount));

        emit RelaySettled(depositHash, msg.sender, relayData);

        // 清理状态存储并获得gas退款。
				// 这也可以防止 `priceDisputed()` 重置这个新的 Finalized 中继状态。
        delete instantRelays[instantRelayHash];
    }

    function _allocateLpFees(uint256 allocatedLpFees) internal {
        undistributedLpFees += allocatedLpFees;
        utilizedReserves += int256(allocatedLpFees);
    }

至此,我们分析完了 Across 合约的主要功能的代码。

合约部署

部署合约目录 deploy 下包含 8 脚本,依次部署了管理合约,WETH 桥接池,Optimism,Arbitrum和Boba的信使,以及 Arbitrum,Optimism 和 Boba 的存款合约。由于过程比较简单,这里就不仔细分析了。

deploy/
├── 001_deploy_across_bridge_admin.js
├── 002_deploy_across_weth_bridge_pool.js
├── 003_deploy_across_optimism_wrapper.js
├── 004_deploy_across_optimism_messenger.js
├── 005_deploy_across_arbitrum_messenger.js
├── 006_deploy_across_boba_messenger.js
├── 007_deploy_across_ovm_bridge_deposit_box.js
└── 008_deploy_across_avm_deposit_box.js

总结

Across 协议整体结构简单,流程清晰,支持了 Across 协议安全,快速的从 L2 向 L1 的资金转移。

代码中调用了 Optimistic Oracle 的接口来出和解决争议,对应的逻辑有空之后详说。

@qiwihui qiwihui added the 区块链 区块链相关 label Mar 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
区块链 区块链相关
Projects
None yet
Development

No branches or pull requests

1 participant