Celo Optics Bridge 代码解析

1. 引言

Celo系列博客有:

  • Celo中的随机数
  • Chorus One:bridge between Cosmos and Celo
  • Celo生态图

前序博客有:

  • Optics Bridge:Celo <-> 以太坊

Optics Bridge开源代码见:

  • https://github.com/celo-org/optics-monorepo(Rust & Solidity等)

Optics中:

  • 以Solidity语言实现了链上合约
  • 以Rust语言实现了链下系统agents

未来将针对NEAR和Solana实现Rust版本的链上合约。

Optics Bridge当前已部署在Celo、以太坊和Polygon主网上。
Celo、以太坊、Polygon主网上的Home及其它核心合约地址见:

  • https://github.com/celo-org/optics-monorepo/tree/main/rust/config/mainnet【V1】
  • https://github.com/celo-org/optics-monorepo/blob/main/rust/config/production-community/【V2】

Optics V2中,向以太坊上各Replica合约提交proveAndProcess请求的费用需由用户承担,而不再由Optics社区运营的processor来承担:【Notice: Optics will no longer cover processing fees when returning to Ethereum.】【Cleo、Polygon上的prove/process操作由相应的processor承担】

  • https://etherscan.io/tx/0xe67f9b18d761a8cf61b0b83686e98beaf34562a572695eb632f5a0e539f568e5【以太坊】
  • https://polygonscan.com/tx/0x81bc053af25dd5adc9f3876494f36f3a028479480234853b35ab1162d7baca08【Polygon】

当前各链分配了唯一的Domian ID:

  • Celo:Domain ID为1667591279
  • 以太坊:Domain ID为6648936
  • Polygon:Domain ID为1886350457
  • Avalanche:Domain ID为1635148152

目前,Home合约中只能配置一个Updater,仅能由UpdaterManager合约来设置:【并不是随机选择updater来签名。且目前对Updater的bond/slash功能并未实现。】

	/**
     * @notice Set a new Updater
     * @param _updater the new Updater
     */
    function setUpdater(address _updater) external onlyUpdaterManager {
        _setUpdater(_updater);
    }

	/**
     * @notice Set the address of a new updater
     * @dev only callable by trusted owner
     * @param _updaterAddress The address of the new updater
     */
    function setUpdater(address _updaterAddress) external onlyOwner {
        _updater = _updaterAddress;
        Home(home).setUpdater(_updaterAddress);
    }

Celo Optics Bridge 代码解析_第1张图片

Optics会在链之间建立communication channels,但是这些channels由xApp(又名“cross-chain applications”) developers来使用。
在 https://github.com/celo-org/optics-monorepo 提供了标准的模式来集成Optics channels,来确保communication是安全的。

集成过程中,需要以下关键要素:

  • 1)已部署在链上的一个Home合约和任意多个Replica合约。这些合约管理Optics communication channels,可由xApp用于send and receive messages。
  • 2)XAppConnectionManager合约:将xApp连接到Optics,允许xApp admin 注册新的Home和Replica合约。对channel进行注册和取消注册是确保xApp正确处理消息的主要方法。xApp可部署自己的connection manager,或者与其它xApps共享connection manager。
/**
 * @title XAppConnectionManager
 * @author Celo Labs Inc.
 * @notice Manages a registry of local Replica contracts
 * for remote Home domains. Accepts Watcher signatures
 * to un-enroll Replicas attached to fraudulent remote Homes
 */
contract XAppConnectionManager is Ownable {
    // ============ Public Storage ============

    // Home contract
    Home public home;
    // local Replica address => remote Home domain
    mapping(address => uint32) public replicaToDomain;
    // remote Home domain => local Replica address
    mapping(uint32 => address) public domainToReplica;
    // watcher address => replica remote domain => has/doesn't have permission
    mapping(address => mapping(uint32 => bool)) private watcherPermissions;

    // ============ Events ============

    /**
     * @notice Emitted when a new Replica is enrolled / added
     * @param domain the remote domain of the Home contract for the Replica
     * @param replica the address of the Replica
     */
    event ReplicaEnrolled(uint32 indexed domain, address replica);

    /**
     * @notice Emitted when a new Replica is un-enrolled / removed
     * @param domain the remote domain of the Home contract for the Replica
     * @param replica the address of the Replica
     */
    event ReplicaUnenrolled(uint32 indexed domain, address replica);

    /**
     * @notice Emitted when Watcher permissions are changed
     * @param domain the remote domain of the Home contract for the Replica
     * @param watcher the address of the Watcher
     * @param access TRUE if the Watcher was given permissions, FALSE if permissions were removed
     */
    event WatcherPermissionSet(
        uint32 indexed domain,
        address watcher,
        bool access
    );

    // ============ Modifiers ============

    modifier onlyReplica() {
        require(isReplica(msg.sender), "!replica");
        _;
    }
    .........
}    
  • 3)Message库:Optics在链之间发送的是raw byte arrays。xApp必须定义一种message specification,使得发送时可serialized,在remote chain上进行处理时可deserialized。
  • 4)Router合约:Router合约会在Optics cross-chain message format 与 本地链合约调用 之间进行转换。Router合约中同时实现了xApp的business logic。Router合约:
    • 公开了面向用户的接口
    • 处理来自其它链的messages
    • 派发将要送到其它链的messages

BridgeRouter合约,若 liquidity provider为终端用户提供快速流动性preFill,可获得0.05%的手续费:

	// 5 bps (0.05%) hardcoded fast liquidity fee. Can be changed by contract upgrade
    uint256 public constant PRE_FILL_FEE_NUMERATOR = 9995;
    uint256 public constant PRE_FILL_FEE_DENOMINATOR = 10000;

	/**
     * @notice Allows a liquidity provider to give an
     * end user fast liquidity by pre-filling an
     * incoming transfer message.
     * Transfers tokens from the liquidity provider to the end recipient, minus the LP fee;
     * Records the liquidity provider, who receives
     * the full token amount when the transfer message is handled.
     * @dev fast liquidity can only be provided for ONE token transfer
     * with the same (recipient, amount) at a time.
     * in the case that multiple token transfers with the same (recipient, amount)
     * @param _message The incoming transfer message to pre-fill
     */
    function preFill(bytes calldata _message) external {
        // parse tokenId and action from message
        bytes29 _msg = _message.ref(0).mustBeMessage();
        bytes29 _tokenId = _msg.tokenId().mustBeTokenId();
        bytes29 _action = _msg.action().mustBeTransfer();
        // calculate prefill ID
        bytes32 _id = _preFillId(_tokenId, _action);
        // require that transfer has not already been pre-filled
        require(liquidityProvider[_id] == address(0), "!unfilled");
        // record liquidity provider
        liquidityProvider[_id] = msg.sender;
        // transfer tokens from liquidity provider to token recipient
        IERC20 _token = _mustHaveToken(_tokenId);
        _token.safeTransferFrom(
            msg.sender,
            _action.evmRecipient(),
            _applyPreFillFee(_action.amnt())
        );
    }

Solidity开发者若有兴趣实现自己的Message库和Router合约,可参看optics-xapps中的例子。

当前测试网的部署配置可参看rust/config/目录。

强烈建议xApp admins运行一个watcher进程来 维护其XAppConnectionManager合约 并 guard from fraud。

GovernanceRouter合约:

  • https://etherscan.io/address/0xdfb2a95900d6b7c8aa95f2e46563a5fcfb5505a1/advanced【V1】
  • https://etherscan.io/address/0xe552861e90a42dddc66b508a18a85bceabfcb835/advanced【V2】:对应的合约owner为https://etherscan.io/address/0x52864fcb88d0862fba14508280f9e2dcbad9281f

1.1 Optics关键点

Optics系统sketch为:

  • 1)A “home” chain commits messages in a merkle tree
  • 2)A bonded “updater” attests to the commitment
  • 3)The home chain ensures the attestation is accurate, and slashes if not
  • 4)Attested updates are relayed on any number of “replica” chains, after a time delay

结果通常为二者之一:

  • 1)All replicas have a valid commitment to messages from the home chain。
  • 2)Failure was published before processing, and the updater can be slashed on the home chain。

尽管Optics的安全保证要弱于header-chain validation,但是,可满足大多数应用场景的要求。

Optics为一种新策略,可在不validate header的情况下,进行跨链通讯。
Optics的目标是:

  • 创建a single short piece of state(为32字节hash),并定期更新该state。

该hash值对应为a merkle tree root,在该merkle tree中包含了a set of cross-chain messages being sent by a single chain(在Optics系统中对应为“home” chain)。home chain上的合约可提交messages,这些messages会被放入a merkle tree(可称为“message tree”)。该message tree的root可传送到任意数量的"replica" chains。

相比于对该commitment进行validity prove,Optics选择了put a delay on message receipt,以保证failures are publicly visible。这样可保证协议的参与者有机会在产生实际伤害之前,对failure进行响应。也就是说,相比于阻止the inclusion of bad messages,Optics可确保message recipients可感知该inclusion,并由机会拒绝对bad message进行处理。

为此,home chain中会指定a single “updater”。该updater需质押bond以确保其good behavior。该updater负责为new message tree root生成signed attestation,以确保其为a previous attestation的extend,同时包含了a valid new root of the message set。这些signed attestation会被发送到每个replica。

Replica:会accept an update attestation signed by the updater,并将其放入pending state。当挑战期过后,Replica会接收该attestation中的update,并存储a new local root。由于该root中包含了a commitment of all messages sent by the home chain,因此,这些messages可be proven (using the replica’s root),然后派发到replica chain上的合约。

为new update设置挑战期主要有2个目的:

  • 1)可保证updater的任何misbehavior都可在message被处理之前公开。This guarantees that data necessary for home chain slashing is available for all faults.
  • 2)使得message recipients可选择不对update中的message进行处理。 If an incorrect update is published, recipients always have the information necessary to take defensive measures before any messages can be processed.

2. 基于Optics的xApp

2.1 如何开发xApp(又名“cross-chain applications”)?

Optics 以raw bytes的形式,将messages由one chain发送到another chain。因此,对于希望使用Optics的跨链应用xApp,需要根据其应用场景定义发送和接收messages的规则。
目前,在Home合约中限制的message最长为2KB:

	// Maximum bytes per message = 2 KiB
    // (somewhat arbitrarily set to begin)
    uint256 public constant MAX_MESSAGE_BODY_BYTES = 2 * 2**10;

每个跨链应用必须实现其自己的messaging protocol。通常,将实现了messaging protocol的合约称为该xApp的Router contract。
在Router contract中,必须:

  • 1)维护a permissioned set of the contract(s) on remote chains,表示其可通过Optics接收这些远程链上合约的messages。它可为a single owner of the application on one chain,也可为a registry of other applications implementing the same rules on various chains。
  • 2)以标准格式对messages编码,使得目标链上的Router contract可对其进行解码。
  • 3)处理来自于remote Router contracts的messages。
  • 4)将messages派发到remote Router contracts。

通过在Router contract上实现以上功能,并部署在多条链上,从而可以通用语言和规则创建一个可用的跨链应用。这种跨链应用可将Optics作为跨链信使来相互发送和接收messages。

Optics团队 xApp Template 提供了xApp开发的模板,开发者可基于此实现自己的应用逻辑,并利用an Optics channel for cross-chain communication。

不同链上xApps之间Message Flow流程为:

  • 1)xApp Router A receives a command on chain A
  • 2)xApp Router A encodes (formats) the information into a message
  • 3)xApp Router A sends the message to xApp Router B on chain B via Optics
  • 4)xApp Router B receives the message via Optics
  • 5)xApp Router B decodes (gets) the information from the message and acts on it

为了实现a xApp,需定义跨链所需执行的actions,对于每个action类型:

  • 1)在 xApp Router合约 中:
    • 1.1)实现一个类似doTypeA的函数,来initiate the action from one domain to another (可附加自己的参数和逻辑)。
    • 1.2)在remote domain上,实现相应的_handle函数来接收、解析、执行该类型的message。
    • 1.3)在handle函数中,添加逻辑来route相应类型的incoming message到合适的_handle函数。
  • 2)在Message library合约 中:
    • 2.1)Formatter函数:将information作为Solidity参数,将其编码为a byte vector in a defined format,产生message。
    • 2.2)Identifier函数:输入为a byte vector,若该vector与其他的message类型格式一致,则返回TRUE。
    • 2.3)Getter(s)函数:对message中的information进行解析,以Solidity 参数的形式返回。

2.2 PingPong xApp示例

PingPong xApp 仅供参考,实际请勿部署。

PingPong xApp可initiating PingPong “matches” between two chains. A match consists of “volleys” sent back-and-forth between the two chains via Optics.

The first volley in a match is always a Ping volley.

  • When a Router receives a Ping volley, it returns a Pong.
  • When a Router receives a Pong volley, it returns a Ping.

The Routers keep track of the number of volleys in a given match, and emit events for each Sent and Received volley so that spectators can watch.

library PingPongMessage {
    using TypedMemView for bytes;
    using TypedMemView for bytes29;

    /// @dev Each message is encoded as a 1-byte type distinguisher, a 4-byte
    /// match id, and a 32-byte volley counter. The messages are therefore all
    /// 37 bytes
    enum Types {
        Invalid, // 0
        Ping, // 1
        Pong // 2
    }

    // ============ Formatters ============

    /**
     * @notice Format a Ping volley
     * @param _count The number of volleys in this match
     * @return The encoded bytes message
     */
    function formatPing(uint32 _match, uint256 _count)
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodePacked(uint8(Types.Ping), _match, _count);
    }

    /**
     * @notice Format a Pong volley
     * @param _count The number of volleys in this match
     * @return The encoded bytes message
     */
    function formatPong(uint32 _match, uint256 _count)
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodePacked(uint8(Types.Pong), _match, _count);
    }

    // ============ Identifiers ============

    /**
     * @notice Get the type that the TypedMemView is cast to
     * @param _view The message
     * @return _type The type of the message (either Ping or Pong)
     */
    function messageType(bytes29 _view) internal pure returns (Types _type) {
        _type = Types(uint8(_view.typeOf()));
    }

    /**
     * @notice Determine whether the message contains a Ping volley
     * @param _view The message
     * @return True if the volley is Ping
     */
    function isPing(bytes29 _view) internal pure returns (bool) {
        return messageType(_view) == Types.Ping;
    }

    /**
     * @notice Determine whether the message contains a Pong volley
     * @param _view The message
     * @return True if the volley is Pong
     */
    function isPong(bytes29 _view) internal pure returns (bool) {
        return messageType(_view) == Types.Pong;
    }

    // ============ Getters ============

    /**
     * @notice Parse the match ID sent within a Ping or Pong message
     * @dev The number is encoded as a uint32 at index 1
     * @param _view The message
     * @return The match id encoded in the message
     */
    function matchId(bytes29 _view) internal pure returns (uint32) {
        // At index 1, read 4 bytes as a uint, and cast to a uint32
        return uint32(_view.indexUint(1, 4));
    }

    /**
     * @notice Parse the volley count sent within a Ping or Pong message
     * @dev The number is encoded as a uint256 at index 1
     * @param _view The message
     * @return The count encoded in the message
     */
    function count(bytes29 _view) internal pure returns (uint256) {
        // At index 1, read 32 bytes as a uint
        return _view.indexUint(1, 32);
    }
}
contract PingPongRouter is Router {
    // ============ Libraries ============

    using TypedMemView for bytes;
    using TypedMemView for bytes29;
    using PingPongMessage for bytes29;

    // ============ Mutable State ============
    uint32 nextMatch;

    // ============ Events ============

    event Received(
        uint32 indexed domain,
        uint32 indexed matchId,
        uint256 count,
        bool isPing
    );
    event Sent(
        uint32 indexed domain,
        uint32 indexed matchId,
        uint256 count,
        bool isPing
    );

    // ============ Constructor ============
    constructor(address _xAppConnectionManager) {
        require(false, "example xApp, do not deploy");

        __XAppConnectionClient_initialize(_xAppConnectionManager);
    }

    // ============ Handle message functions ============

    /**
     * @notice Handle "volleys" sent via Optics from other remote PingPong Routers
     * @param _origin The domain the message is coming from
     * @param _sender The address the message is coming from
     * @param _message The message in the form of raw bytes
     */
    function handle(
        uint32 _origin,
        bytes32 _sender,
        bytes memory _message
    ) external override onlyReplica onlyRemoteRouter(_origin, _sender) {
        bytes29 _msg = _message.ref(0);
        if (_msg.isPing()) {
            _handlePing(_origin, _msg);
        } else if (_msg.isPong()) {
            _handlePong(_origin, _msg);
        } else {
            // if _message doesn't match any valid actions, revert
            require(false, "!valid action");
        }
    }

    /**
     * @notice Handle a Ping volley
     * @param _origin The domain that sent the volley
     * @param _message The message in the form of raw bytes
     */
    function _handlePing(uint32 _origin, bytes29 _message) internal {
        bool _isPing = true;
        _handle(_origin, _isPing, _message);
    }

    /**
     * @notice Handle a Pong volley
     * @param _origin The domain that sent the volley
     * @param _message The message in the form of raw bytes
     */
    function _handlePong(uint32 _origin, bytes29 _message) internal {
        bool _isPing = false;
        _handle(_origin, _isPing, _message);
    }

    /**
     * @notice Upon receiving a volley, emit an event, increment the count and return a the opposite volley
     * @param _origin The domain that sent the volley
     * @param _isPing True if the volley received is a Ping, false if it is a Pong
     * @param _message The message in the form of raw bytes
     */
    function _handle(
        uint32 _origin,
        bool _isPing,
        bytes29 _message
    ) internal {
        // get the volley count for this game
        uint256 _count = _message.count();
        uint32 _match = _message.matchId();
        // emit a Received event
        emit Received(_origin, _match, _count, _isPing);
        // send the opposite volley back
        _send(_origin, !_isPing, _match, _count + 1);
    }

    // ============ Dispatch message functions ============

    /**
     * @notice Initiate a PingPong match with the destination domain
     * by sending the first Ping volley.
     * @param _destinationDomain The domain to initiate the match with
     */
    function initiatePingPongMatch(uint32 _destinationDomain) external {
        // the PingPong match always begins with a Ping volley
        bool _isPing = true;
        // increment match counter
        uint32 _match = nextMatch;
        nextMatch = _match + 1;
        // send the first volley to the destination domain
        _send(_destinationDomain, _isPing, _match, 0);
    }

    /**
     * @notice Send a Ping or Pong volley to the destination domain
     * @param _destinationDomain The domain to send the volley to
     * @param _isPing True if the volley to send is a Ping, false if it is a Pong
     * @param _count The number of volleys in this match
     */
    function _send(
        uint32 _destinationDomain,
        bool _isPing,
        uint32 _match,
        uint256 _count
    ) internal {
        // get the xApp Router at the destinationDomain
        bytes32 _remoteRouterAddress = _mustHaveRemote(_destinationDomain);
        // format the ping message
        bytes memory _message = _isPing
            ? PingPongMessage.formatPing(_match, _count)
            : PingPongMessage.formatPong(_match, _count);
        // send the message to the xApp Router
        (_home()).dispatch(_destinationDomain, _remoteRouterAddress, _message);
        // emit a Sent event
        emit Sent(_destinationDomain, _match, _count, _isPing);
    }
}

2.3 Token Bridge xApp示例

Optics Token Bridge为a xApp,为Optics生态应用的一种,用于链之间的token transfer。
Token Bridge的主要特征为:保证token在多条链之间的流通总量保持不变。

部署在Celo、以太坊、Polygon主网上的Token Bridge合约地址见:

  • https://github.com/celo-org/optics-monorepo/tree/main/rust/config/mainnet/bridge/1631143085018【V1】
  • https://github.com/celo-org/optics-monorepo/commit/b90a38a2c9f81d508f9acfffb05b0c592f433c54【V2】

BridgeRouter、Home等合约地址有2套是因为,2021年11月26日,Celo团队对Optics协议进行了升级。

  • Bridge Router V1合约:https://etherscan.io/address/0x67364232a8f8da6f22df3be3408ef9872132f2a6/advanced#internaltx
  • Bridge Router V2合约:https://etherscan.io/address/0x688a54c4b1c5b917154ea2f61b8a4a4cbdff4738/advanced#internaltx
  • Home V1合约:https://etherscan.io/address/0xfac41463ef1e01546f2130f92184a053a0e3fa14/advanced#internaltx
  • Home V2合约:https://etherscan.io/address/0xfc6e146384b5c65f372d5b20537f3e8727ad3723/advanced#internaltx

部署在以太坊上的XAppConnectionManager合约地址为:

  • https://etherscan.io/address/0xcec158a719d11005bd9339865965bed938beafa3/advanced#readContract【v1,部署于2021年9月8日。近期仍有交易。】
  • https://etherscan.io/address/0x8a926ce79f83a5a4c234bee93feafcc85b1e40cd/advanced#internaltx【v2,部署于2021年11月26日,近期交易活跃。】

部署在以太坊上的Replica合约有多个:

  • https://etherscan.io/address/0xfc4060e4fd5979f848b8edc8505d2f89d83b9e04#readContract【v1,2021年9月8日部署。近期仍有内部交易】
  • https://etherscan.io/address/0xcbe8b8c4fe6590bb59d1507de7f252af3e621e58#readContract【v2,2021年11月26日部署。最新的交易截止到2022年2月2日。】
  • https://etherscan.io/address/0x8f6b6adb49cdca3b9f6947f61a1201242c3d827f/advanced#readContract【v2,2022年2月1日部署。近期交易活跃。】

以太坊上wrapped ether合约为:

  • https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2/advanced#internaltx

转账时的tokenID为该token所属源链domain_id+该token address:

uint256 private constant TOKEN_ID_LEN = 36; // 4 bytes domain + 32 bytes id
	
	/**
     * @notice Formats the Token ID
     * @param _domain The domain
     * @param _id The ID
     * @return The formatted Token ID
     */
    function formatTokenId(uint32 _domain, bytes32 _id)
        internal
        pure
        returns (bytes29)
    {
        return mustBeTokenId(abi.encodePacked(_domain, _id).ref(0));
    }

若某token未在目标链上发布,当前BridgeRouter合约会部署相应的token合约?待细看?【Wormhole V2将部署wrapped token合约的费用拆分出来了,不再由官方合约承担。】

	/**
     * @notice Deploy and initialize a new token contract
     * @dev Each token contract is a proxy which
     * points to the token upgrade beacon
     * @return _token the address of the token contract
     */
    function _deployToken(bytes29 _tokenId) internal returns (address _token) {
        // deploy and initialize the token contract
        _token = address(new UpgradeBeaconProxy(tokenBeacon, ""));
        // initialize the token separately from the
        IBridgeToken(_token).initialize();
        // set the default token name & symbol
        string memory _name;
        string memory _symbol;
        (_name, _symbol) = _defaultDetails(_tokenId);
        IBridgeToken(_token).setDetails(_name, _symbol, 18);
        // store token in mappings
        representationToCanonical[_token].domain = _tokenId.domain();
        representationToCanonical[_token].id = _tokenId.id();
        canonicalToRepresentation[_tokenId.keccak()] = _token;
        // emit event upon deploying new token
        emit TokenDeployed(_tokenId.domain(), _tokenId.id(), _token);
    }

Optics部署采用可升级配置,由proxy contracts指向implementation contracts,使得可在无需迁移contract state的情况下,通过governance对contract implementation进行升级。

详细的跨链token transfer流程可参看:

  • 2021年9月博客 Optics: How to Ape ERC-20 Tokens With Etherscan

message处理规则为:

  • 1)BridgeRouter合约仅接收messages from other remote BridgeRouter合约,每个BridgeRouter合约需注册其支持的remote BridgeRouter合约,使得:
    • 所接收到的message符合local BridgeRouter合约的规则。
    • 消息中的任何sent token都是有效的,因为发送该消息的remote BridgeRouter在发送消息之前,在其本地就应强化确认用户已托管了相应的token。
	/**
     * @notice Register the address of a Router contract for the same xApp on a remote chain
     * @param _domain The domain of the remote xApp Router
     * @param _router The address of the remote xApp Router
     */
    function enrollRemoteRouter(uint32 _domain, bytes32 _router)
        external
        onlyOwner
    {
        remotes[_domain] = _router;
    }
  • 2)来自于remote BridgeRouter合约的messages必须通过Optics发送,经由local Replica合约派发。Replica地址与Domain映射关系可通过XAppConnectionManager合约注册。
    BridgeRouter依赖XAppConnectionManager合约for a valid registry of local Replicas。onlyReplica表示仅可由Replica合约调用。
	modifier onlyReplica() {
        require(isReplica(msg.sender), "!replica");
        _;
    }
    
    /**
     * @notice Check whether _replica is enrolled
     * @param _replica the replica to check for enrollment
     * @return TRUE iff _replica is enrolled
     */
    function isReplica(address _replica) public view returns (bool) {
        return replicaToDomain[_replica] != 0;
    }
    
	/**
     * @notice Handles an incoming message
     * @param _origin The origin domain
     * @param _sender The sender address
     * @param _message The message
     */
    function handle(
        uint32 _origin,
        bytes32 _sender,
        bytes memory _message
    ) external override onlyReplica onlyRemoteRouter(_origin, _sender) {
        // parse tokenId and action from message
        bytes29 _msg = _message.ref(0).mustBeMessage();
        bytes29 _tokenId = _msg.tokenId();
        bytes29 _action = _msg.action();
        // handle message based on the intended action
        if (_action.isTransfer()) {
            _handleTransfer(_tokenId, _action);
        } else if (_action.isDetails()) {
            _handleDetails(_tokenId, _action);
        } else if (_action.isRequestDetails()) {
            _handleRequestDetails(_origin, _sender, _tokenId);
        } else {
            require(false, "!valid action");
        }
    }
  • 3)若从源链发来的token为目标链上的native token,则会从Router合约的托管池中向目标链上的收款方发送相应数额的token。
  • 4)若从源链发来的token不是目标链上的native token,则:
    • 4.1)可检查目标链上,Router合约是否已部署a representation token contract。若没有,则部署一个representation token contract,然后在token registry中添加该地址。
    • 4.2)在目标链上mint 相应数量的representation tokens,并将其发送到目标链的收款方。

Message派发规则为:

  • 1)TODO:规则为——用户必须approve token to Router on local chain (if it’s a native token) proving they have ownership over that token and can send to the native chain。【ETH native token目前是借助ETHHelper合约的sendTo接口。先存入ETHHelper合约,只需对ETHHelper合约进行一次无限额的approve,而不需要每个用户都approve。不过不限额有风险,未来还是应修改为由用户来approve】
	constructor(address _weth, address _bridge) {
        weth = IWeth(_weth);
        bridge = BridgeRouter(_bridge);
        IWeth(_weth).approve(_bridge, uint256(-1));
    }
	/**
     * @notice Sends ETH over the Optics Bridge. Sends to a specified EVM
     * address on the other side.
     * @dev This function should only be used when sending TO an EVM-like
     * domain. As with all bridges, improper use may result in loss of funds
     * @param _domain The domain to send funds to.
     * @param _to The EVM address of the recipient
     */
    function sendToEVMLike(uint32 _domain, address _to) external payable {
        sendTo(_domain, TypeCasts.addressToBytes32(_to));
    }
    /**
     * @notice Sends ETH over the Optics Bridge. Sends to a full-width Optics
     * identifer on the other side.
     * @dev As with all bridges, improper use may result in loss of funds.
     * @param _domain The domain to send funds to.
     * @param _to The 32-byte identifier of the recipient
     */
    function sendTo(uint32 _domain, bytes32 _to) public payable {
        weth.deposit{value: msg.value}();
        bridge.send(address(weth), msg.value, _domain, _to);
    }
  • 2)sending tokens:【ERC20 token可直接调用BridgeRouter合约的send函数接口。】
    • 2.1)用户使用ERC-20的approve来grant allowance for the tokens being sent to the local BridgeRouter contract。
    • 2.2)用户调用local BridgeRouter合约中的send函数来transfer the tokens to a remote。
    /**
     * @notice Sends ETH over the Optics Bridge. Sends to a full-width Optics
     * identifer on the other side.
     * @dev As with all bridges, improper use may result in loss of funds.
     * @param _domain The domain to send funds to.
     * @param _to The 32-byte identifier of the recipient
     */
    function sendTo(uint32 _domain, bytes32 _to) public payable {
        weth.deposit{value: msg.value}();
        bridge.send(address(weth), msg.value, _domain, _to);
    }
    
    /**
     * @notice Send tokens to a recipient on a remote chain
     * @param _token The token address
     * @param _amount The token amount
     * @param _destination The destination domain
     * @param _recipient The recipient address
     */
    function send(
        address _token,
        uint256 _amount,
        uint32 _destination,
        bytes32 _recipient
    ) external {
        require(_amount > 0, "!amnt");
        require(_recipient != bytes32(0), "!recip");
        // get remote BridgeRouter address; revert if not found
        bytes32 _remote = _mustHaveRemote(_destination);
        // remove tokens from circulation on this chain
        IERC20 _bridgeToken = IERC20(_token);
        if (_isLocalOrigin(_bridgeToken)) {
            // if the token originates on this chain, hold the tokens in escrow
            // in the Router
            _bridgeToken.safeTransferFrom(msg.sender, address(this), _amount);
        } else {
            // if the token originates on a remote chain, burn the
            // representation tokens on this chain
            _downcast(_bridgeToken).burn(msg.sender, _amount);
        }
        // format Transfer Tokens action
        bytes29 _action = BridgeMessage.formatTransfer(_recipient, _amount);
        // send message to remote chain via Optics
        Home(xAppConnectionManager.home()).dispatch(
            _destination,
            _remote,
            BridgeMessage.formatMessage(_formatTokenId(_token), _action)
        );
        // emit Send event to record token sender
        emit Send(
            address(_bridgeToken),
            msg.sender,
            _destination,
            _recipient,
            _amount
        );
    }
    
  • 3)若收到的token为源链上的native token,则会将相应数量存入源链BridgeRouter合约资金池中。
  • 4)若收到的token不是源链上的native token,则第一次时,源链上的BridgeRouter合约会部署一个相应的representation token contract。BridgeRouter合约在发送token之前,会burn相应数额的representation token。

Message格式:

  • 表示的是针对此应用的消息编码格式。

2.3.1 token bridge xApp架构

主要包括3大合约内容:

  • 1)BridgeRouter合约
  • 2)TokenRegistry abstract合约:BridgeRouter合约会继承该合约。
  • 3)BridgeMessage library

具体为:

  • 1)BridgeRouter合约:

    • 1.1)负责从本链的Replica合约中接收其他链发来的sending token messages。仅可由注册的Replica合约调用其handle接口。
    /**
     * @notice Given formatted message, attempts to dispatch
     * message payload to end recipient.
     * @dev Recipient must implement a `handle` method (refer to IMessageRecipient.sol)
     * Reverts if formatted message's destination domain is not the Replica's domain,
     * if message has not been proven,
     * or if not enough gas is provided for the dispatch transaction.
     * @param _message Formatted message
     * @return _success TRUE iff dispatch transaction succeeded
     */
    function process(bytes memory _message) public returns (bool _success) {
        bytes29 _m = _message.ref(0);
        // ensure message was meant for this domain
        require(_m.destination() == localDomain, "!destination");
        // ensure message has been proven
        bytes32 _messageHash = _m.keccak();
        require(messages[_messageHash] == MessageStatus.Proven, "!proven");
        // check re-entrancy guard
        require(entered == 1, "!reentrant");
        entered = 0;
        // update message status as processed
        messages[_messageHash] = MessageStatus.Processed;
        // A call running out of gas TYPICALLY errors the whole tx. We want to
        // a) ensure the call has a sufficient amount of gas to make a
        //    meaningful state change.
        // b) ensure that if the subcall runs out of gas, that the tx as a whole
        //    does not revert (i.e. we still mark the message processed)
        // To do this, we require that we have enough gas to process
        // and still return. We then delegate only the minimum processing gas.
        require(gasleft() >= PROCESS_GAS + RESERVE_GAS, "!gas");
        // get the message recipient
        address _recipient = _m.recipientAddress();
        // set up for assembly call
        uint256 _toCopy;
        uint256 _maxCopy = 256;
        uint256 _gas = PROCESS_GAS;
        // allocate memory for returndata
        bytes memory _returnData = new bytes(_maxCopy);
        bytes memory _calldata = abi.encodeWithSignature(
            "handle(uint32,bytes32,bytes)",
            _m.origin(),
            _m.sender(),
            _m.body().clone()
        );
        // dispatch message to recipient
        // by assembly calling "handle" function
        // we call via assembly to avoid memcopying a very large returndata
        // returned by a malicious contract
        assembly {
            _success := call(
                _gas, // gas
                _recipient, // recipient
                0, // ether value
                add(_calldata, 0x20), // inloc
                mload(_calldata), // inlen
                0, // outloc
                0 // outlen
            )
            // limit our copy to 256 bytes
            _toCopy := returndatasize()
            if gt(_toCopy, _maxCopy) {
                _toCopy := _maxCopy
            }
            // Store the length of the copied bytes
            mstore(_returnData, _toCopy)
            // copy the bytes from returndata[0:_toCopy]
            returndatacopy(add(_returnData, 0x20), 0, _toCopy)
        }
        // emit process results
        emit Process(_messageHash, _success, _returnData);
        // reset re-entrancy guard
        entered = 1;
    }
    
    • 1.2)将由本链发起的向其它链的sending token message 派发到本链的Home合约中。BridgeRouter合约会调用Home合约的dispatch接口。
    /**
     * @notice Dispatch the message it to the destination domain & recipient
     * @dev Format the message, insert its hash into Merkle tree,
     * enqueue the new Merkle root, and emit `Dispatch` event with message information.
     * @param _destinationDomain Domain of destination chain
     * @param _recipientAddress Address of recipient on destination chain as bytes32
     * @param _messageBody Raw bytes content of message
     */
    function dispatch(
        uint32 _destinationDomain,
        bytes32 _recipientAddress,
        bytes memory _messageBody
    ) external notFailed {
        require(_messageBody.length <= MAX_MESSAGE_BODY_BYTES, "msg too long");
        // get the next nonce for the destination domain, then increment it
        uint32 _nonce = nonces[_destinationDomain];
        nonces[_destinationDomain] = _nonce + 1;
        // format the message into packed bytes
        bytes memory _message = Message.formatMessage(
            localDomain,
            bytes32(uint256(uint160(msg.sender))),
            _nonce,
            _destinationDomain,
            _recipientAddress,
            _messageBody
        );
        // insert the hashed message into the Merkle tree
        bytes32 _messageHash = keccak256(_message);
        tree.insert(_messageHash);
        // enqueue the new Merkle root after inserting the message
        queue.enqueue(root());
        // Emit Dispatch event with message information
        // note: leafIndex is count() - 1 since new leaf has already been inserted
        emit Dispatch(
            _messageHash,
            count() - 1,
            _destinationAndNonce(_destinationDomain, _nonce),
            committedRoot,
            _message
        );
    }
    
    • 1.3)管理在本链部署的representation ERC-20 token合约registry。所谓tightly-packed,是指canonicalToRepresentation[_tokenId.keccak()] = _token;
    // UpgradeBeacon from which new token proxies will get their implementation
    address public tokenBeacon;
    // local representation token address => token ID
    mapping(address => TokenId) public representationToCanonical;
    // hash of the tightly-packed TokenId => local representation token address
    // If the token is of local origin, this MUST map to address(0).
    mapping(bytes32 => address) public canonicalToRepresentation;
    
    // Tokens are identified by a TokenId:
    // domain - 4 byte chain ID of the chain from which the token originates
    // id - 32 byte identifier of the token address on the origin chain, in that chain's address format
    struct TokenId {
        uint32 domain;
        bytes32 id;
    }
    
    • 1.4)维护remote BridgeRouter合约registry,使得:
      • 1.4.1)本链仅接收来自已注册认证的remote BridgeRouter合约消息。
      • 1.4.2)可正确处理发送到remote BridgeRouter合约的消息
  • 2)TokenRegistry合约:

    • 2.1)负责在本链部署和跟踪representation ERC-20 token合约。
    /**
     * @notice Deploy and initialize a new token contract
     * @dev Each token contract is a proxy which
     * points to the token upgrade beacon
     * @return _token the address of the token contract
     */
    function _deployToken(bytes29 _tokenId) internal returns (address _token) {
        // deploy and initialize the token contract
        _token = address(new UpgradeBeaconProxy(tokenBeacon, ""));
        // initialize the token separately from the
        IBridgeToken(_token).initialize();
        // set the default token name & symbol
        string memory _name;
        string memory _symbol;
        (_name, _symbol) = _defaultDetails(_tokenId);
        IBridgeToken(_token).setDetails(_name, _symbol, 18);
        // store token in mappings
        representationToCanonical[_token].domain = _tokenId.domain();
        representationToCanonical[_token].id = _tokenId.id();
        canonicalToRepresentation[_tokenId.keccak()] = _token;
        // emit event upon deploying new token
        emit TokenDeployed(_tokenId.domain(), _tokenId.id(), _token);
    }
    
    • 2.2)当transfer新的token时,会在本链相应部署一个新的representation token合约,并存储源链上的token address以及本链上的representation合约地址 map信息。具体为:
    	// store token in mappings
        representationToCanonical[_token].domain = _tokenId.domain();
        representationToCanonical[_token].id = _tokenId.id();
        canonicalToRepresentation[_tokenId.keccak()] = _token;
    
    • 2.3)BridgeRouter合约集成了TokenRegistry合约。BridgeRouter可确保在mint/burn之前,相应的token representation在本链确实存在。
    contract BridgeRouter is Version0, Router, TokenRegistry {.....}
    /**
     * @notice Get the local token address
     * for the canonical token represented by tokenID
     * Returns address(0) if canonical token is of remote origin
     * and no representation token has been deployed locally
     * @param _tokenId the token id of the canonical token
     * @return _local the local token address
     */
    function _getTokenAddress(bytes29 _tokenId)
        internal
        view
        returns (address _local)
    {
        if (_tokenId.domain() == _localDomain()) {
            // Token is of local origin
            _local = _tokenId.evmId();
        } else {
            // Token is a representation of a token of remote origin
            _local = canonicalToRepresentation[_tokenId.keccak()];
        }
    }
    function _ensureToken(bytes29 _tokenId) internal returns (IERC20) {
        address _local = _getTokenAddress(_tokenId);
        if (_local == address(0)) {
            // Representation does not exist yet;
            // deploy representation contract
            _local = _deployToken(_tokenId);
            // message the origin domain
            // to request the token details
            _requestDetails(_tokenId);
        }
        return IERC20(_local);
    }
    /**
     * @notice Handles an incoming Transfer message.
     *
     * If the token is of local origin, the amount is sent from escrow.
     * Otherwise, a representation token is minted.
     *
     * @param _tokenId The token ID
     * @param _action The action
     */
    function _handleTransfer(bytes29 _tokenId, bytes29 _action) internal {
        // get the token contract for the given tokenId on this chain;
        // (if the token is of remote origin and there is
        // no existing representation token contract, the TokenRegistry will
        // deploy a new one)
        IERC20 _token = _ensureToken(_tokenId);
        ........
        }
    /**
     * @notice Enroll a custom token. This allows projects to work with
     * governance to specify a custom representation.
     * @dev This is done by inserting the custom representation into the token
     * lookup tables. It is permissioned to the owner (governance) and can
     * potentially break token representations. It must be used with extreme
     * caution.
     * After the token is inserted, new mint instructions will be sent to the
     * custom token. The default representation (and old custom representations)
     * may still be burnt. Until all users have explicitly called migrate, both
     * representations will continue to exist.
     * The custom representation MUST be trusted, and MUST allow the router to
     * both mint AND burn tokens at will.
     * @param _id the canonical ID of the Token to enroll, as a byte vector
     * @param _custom the address of the custom implementation to use.
     */
    function enrollCustom(
        uint32 _domain,
        bytes32 _id,
        address _custom
    ) external onlyOwner {
        // Sanity check. Ensures that human error doesn't cause an
        // unpermissioned contract to be enrolled.
        IBridgeToken(_custom).mint(address(this), 1);
        IBridgeToken(_custom).burn(address(this), 1);
        // update mappings with custom token
        bytes29 _tokenId = BridgeMessage.formatTokenId(_domain, _id);
        representationToCanonical[_custom].domain = _tokenId.domain();
        representationToCanonical[_custom].id = _tokenId.id();
        bytes32 _idHash = _tokenId.keccak();
        canonicalToRepresentation[_idHash] = _custom;
    }
    
  • 3)BridgeMessage library:用于以标准化方式处理所有编码/解码信息的库,以便通过Optics发送。

2.3.2 token Transfer的message flow流程

由链A将token发送到链B的message flow流程,主要参与方有:

  • 1)链A端
  • 2)链下服务:Updater、Relayer、Processor、Watcher
  • 3)链B端

详细流程为:

  • 1)链A端

    • 1.1)用户想要将其在链A的token发送到链B。
      • 若为native token,则用户必须首先approve相应数量的token到本链的BridgeRouter-A合约。
    • 1.2)用户调用本链的BridgeRouter-A合约。
      • 1.2.1)若为native token,则会将相应数量的token由用户钱包转入到BridgeRouter-A合约的资金池。
      • 1.2.2)若为non-native token,则BridgeRouter-A合约会从用户钱包中burn相应数量的token。
        • 注意:BridgeRouter-A token可burn non-native tokens,是因为相应的representation合约是由BridgeRouter-A部署的,BridgeRouter-A合约具有相应representation合约的administrator权限。
    • 1.3)BridgeRouter-ABridgeRouter-B构建相应的messages。
      • BridgeRouter-A合约中会维护其它链上的BridgeRouter合约map,使得其知道应往哪发送链B的message。
       // get remote BridgeRouter address; revert if not found
      bytes32 _remote = _mustHaveRemote(_destination);
      
      // format Transfer Tokens action
      bytes29 _action = BridgeMessage.formatTransfer(_recipient, _amount);
      // send message to remote chain via Optics
      Home(xAppConnectionManager.home()).dispatch(
          _destination,
          _remote,
          BridgeMessage.formatMessage(_formatTokenId(_token), _action)
      );
      
      // get the next nonce for the destination domain, then increment it
      uint32 _nonce = nonces[_destinationDomain];
      nonces[_destinationDomain] = _nonce + 1;
      // format the message into packed bytes
      bytes memory _message = Message.formatMessage(
          localDomain,
          bytes32(uint256(uint160(msg.sender))),
          _nonce,
          _destinationDomain,
          _recipientAddress,
          _messageBody
      );
      // insert the hashed message into the Merkle tree
      bytes32 _messageHash = keccak256(_message);
      tree.insert(_messageHash);
      // enqueue the new Merkle root after inserting the message
      queue.enqueue(root());
      
    • 1.4)BridgeRouter-A合约会调用Home-A合约的enqueue接口来将消息发送到链B。
    	// insert the hashed message into the Merkle tree
        bytes32 _messageHash = keccak256(_message);
        tree.insert(_messageHash);
        // enqueue the new Merkle root after inserting the message
        queue.enqueue(root());
        // Emit Dispatch event with message information
        // note: leafIndex is count() - 1 since new leaf has already been inserted
        emit Dispatch(
            _messageHash,
            count() - 1,
            _destinationAndNonce(_destinationDomain, _nonce),
            committedRoot,
            _message
        );
    
  • 2)链下服务:标准的流程为Updater->Relayer->Processor。

    • 2.1)Updater链下服务:会确定当前链A的Home合约中配置的Updater为其自身,然后会定期查询Home合约的suggestUpdate来获取最新待处理的消息。在UpdateProducer线程中,会进行判断,符合条件会对[committedRoot, new]进行签名,并存储在Updater本地数据库中。
      UpdateSubmitter线程中,会定期从本地数据库中获取已签名的update,调用Home合约的update接口提交。
    /**
     * @notice Suggest an update for the Updater to sign and submit.
     * @dev If queue is empty, null bytes returned for both
     * (No update is necessary because no messages have been dispatched since the last update)
     * @return _committedRoot Latest root signed by the Updater
     * @return _new Latest enqueued Merkle root
     */
    function suggestUpdate()
        external
        view
        returns (bytes32 _committedRoot, bytes32 _new)
    {
        if (queue.length() != 0) {
            _committedRoot = committedRoot;
            _new = queue.lastItem();
        }
    }
    
    
      				// If the suggested matches our local view, sign an update
                    // and store it as locally produced
                    let signed = suggested.sign_with(self.signer.as_ref()).await?;
    				
    				self.db.store_produced_update(&signed)?;
    
    pub struct Update {
     /// The home chain
     pub home_domain: u32,
     /// The previous root
     pub previous_root: H256,
     /// The new root
     pub new_root: H256,
    }
     fn signing_hash(&self) -> H256 {
         // sign:
         // domain(home_domain) || previous_root || new_root
         H256::from_slice(
             Keccak256::new()
                 .chain(home_domain_hash(self.home_domain))
                 .chain(self.previous_root)
                 .chain(self.new_root)
                 .finalize()
                 .as_slice(),
         )
     }
    /// Sign an update using the specified signer
     pub async fn sign_with<S: Signer>(self, signer: &S) -> Result<SignedUpdate, S::Error> {
         let signature = signer
             .sign_message_without_eip_155(self.signing_hash())
             .await?;
         Ok(SignedUpdate {
             update: self,
             signature,
         })
     }
    
    • 2.2)Relayer链下服务:读取链B的replica合约的committedRoot,基于该committedRoot,从本地数据库中读取【???跟Updater共用一个数据库???而不是监听Home合约的Update(localDomain, _committedRoot, _newRoot, _signature)事件??】已签名的update,调用Replica合约的update接口。
    • 2.3)Processor链下服务:按本domain的nonce顺序,从数据库中获取相应待处理的已签名update消息(self.db.message_by_nonce(destination, nonce)),并从数据库中获取相应的merkle tree proof(self.db.proof_by_leaf_index(message.leaf_index)),以及leaf index信息。【代码中有设置deny list 黑名单。】调用链B的Replica合约的acceptableRoot接口,若已过挑战期,则返回true。若已过挑战期,Processor会将根据消息类型,调用链B的Replica合约的prove_and_processprocess接口:【Processor会在本地维护merkle tree with all leaves。】
    // The basic structure of this loop is as follows:
    // 1. Get the last processed index
    // 2. Check if the Home knows of a message above that index
    //      - If not, wait and poll again
    // 3. Check if we have a proof for that message
    //      - If not, wait and poll again
    // 4. Check if the proof is valid under the replica
    // 5. Submit the proof to the replica
    
    /// Dispatch a message for processing. If the message is already proven, process only.
    async fn process(&self, message: CommittedMessage, proof: Proof) -> Result<()> {
        use optics_core::Replica;
        let status = self.replica.message_status(message.to_leaf()).await?;
    
        match status {
            MessageStatus::None => {
                self.replica
                    .prove_and_process(message.as_ref(), &proof)
                    .await?;
            }
            MessageStatus::Proven => {
                self.replica.process(message.as_ref()).await?;
            }
            MessageStatus::Processed => {
                info!(
                    domain = message.message.destination,
                    nonce = message.message.nonce,
                    leaf_index = message.leaf_index,
                    leaf = ?message.message.to_leaf(),
                    "Message {}:{} already processed",
                    message.message.destination,
                    message.message.nonce
                );
                return Ok(());
            }
        }
    
        info!(
            domain = message.message.destination,
            nonce = message.message.nonce,
            leaf_index = message.leaf_index,
            leaf = ?message.message.to_leaf(),
            "Processed message. Destination: {}. Nonce: {}. Leaf index: {}.",
            message.message.destination,
            message.message.nonce,
            message.leaf_index,
        );
        Ok(())
    }
    
  • 3)链B端:

    • 3.1)挑战期过后,链B的replica合约会处理消息,并dispatch到链B的BridgeRouter合约。
    	// dispatch message to recipient
        // by assembly calling "handle" function
        // we call via assembly to avoid memcopying a very large returndata
        // returned by a malicious contract
        assembly {
            _success := call(
                _gas, // gas
                _recipient, // recipient
                0, // ether value
                add(_calldata, 0x20), // inloc
                mload(_calldata), // inlen
                0, // outloc
                0 // outlen
            )
            // limit our copy to 256 bytes
            _toCopy := returndatasize()
            if gt(_toCopy, _maxCopy) {
                _toCopy := _maxCopy
            }
            // Store the length of the copied bytes
            mstore(_returnData, _toCopy)
            // copy the bytes from returndata[0:_toCopy]
            returndatacopy(add(_returnData, 0x20), 0, _toCopy)
        }
    
    • 3.2)链B的BridgeRouter合约中维护了其在本链信任的Replica合约mapping,以此来接收证实来自于链A的message。onlyReplica
    • 3.3)链B的BridgeRouter合约中维护了其它链的BridgeRouter合约mapping,以此来接收证实来自于链A BridgeRouter的message。onlyRemoteRouter(_origin, _sender)
    • 3.4)链B的BridgeRouter合约会查找其registry中相应的ERC-20 token合约,若不存在,会部署一个新的representative one。
    function _ensureToken(bytes29 _tokenId) internal returns (IERC20) {
        address _local = _getTokenAddress(_tokenId);
        if (_local == address(0)) {
            // Representation does not exist yet;
            // deploy representation contract
            _local = _deployToken(_tokenId);
            // message the origin domain
            // to request the token details
            _requestDetails(_tokenId);
        }
        return IERC20(_local);
    }
    
    • 3.5)链B的BridgeRouter将token发送到相应的接收方。若为native token,则从BridgeRouter资金池中将金额转给收款方;若为non-native token,则BridgeRouter合约给收款方mint相应数量的non-native tokens。BridgeRouter可mint是因为相应的non-native token合约是其部署的,其具有相应合约的管理员权限。
    /**
     * @notice Handles an incoming message
     * @param _origin The origin domain
     * @param _sender The sender address
     * @param _message The message
     */
    function handle(
        uint32 _origin,
        bytes32 _sender,
        bytes memory _message
    ) external override onlyReplica onlyRemoteRouter(_origin, _sender) {
        // parse tokenId and action from message
        bytes29 _msg = _message.ref(0).mustBeMessage();
        bytes29 _tokenId = _msg.tokenId();
        bytes29 _action = _msg.action();
        // handle message based on the intended action
        if (_action.isTransfer()) {
            _handleTransfer(_tokenId, _action);
        } else if (_action.isDetails()) {
            _handleDetails(_tokenId, _action);
        } else if (_action.isRequestDetails()) {
            _handleRequestDetails(_origin, _sender, _tokenId);
        } else {
            require(false, "!valid action");
        }
    }
    /**
     * @notice Handles an incoming Transfer message.
     *
     * If the token is of local origin, the amount is sent from escrow.
     * Otherwise, a representation token is minted.
     *
     * @param _tokenId The token ID
     * @param _action The action
     */
    function _handleTransfer(bytes29 _tokenId, bytes29 _action) internal {
        // get the token contract for the given tokenId on this chain;
        // (if the token is of remote origin and there is
        // no existing representation token contract, the TokenRegistry will
        // deploy a new one)
        IERC20 _token = _ensureToken(_tokenId);
        address _recipient = _action.evmRecipient();
        // If an LP has prefilled this token transfer,
        // send the tokens to the LP instead of the recipient
        bytes32 _id = _preFillId(_tokenId, _action);
        address _lp = liquidityProvider[_id];
        if (_lp != address(0)) {
            _recipient = _lp;
            delete liquidityProvider[_id];
        }
        // send the tokens into circulation on this chain
        if (_isLocalOrigin(_token)) {
            // if the token is of local origin, the tokens have been held in
            // escrow in this contract
            // while they have been circulating on remote chains;
            // transfer the tokens to the recipient
            _token.safeTransfer(_recipient, _action.amnt());
        } else {
            // if the token is of remote origin, mint the tokens to the
            // recipient on this chain
            _downcast(_token).mint(_recipient, _action.amnt());
        }
    }
    

3. Optics message流程

Optics当前仍在迭代开发中。
Optics会将messages进行batch,仅发送tree roots,因此,一旦message被传入到Home合约,就无法在链上跟踪各个单独的message。可开发一个agent-querying工具,向链下agents query单独的每笔交易,当前并没有相应的工具存在。

因此,这意味着在send和receipt之间,存在a state of unknown,可将其看成是snail mail,尽管无法跟踪,但是由delivery confirmation。在链上可确认的事情仅有:

  • 1)A transaction was sent on chain A to the BridgeRouter合约。
  • 2)链B的收款方收到了相应的token。

详细的message伪追踪流程为:

  • 1)查看生产环境的各合约地址。
  • 2)验证在源链上的某笔交易被发送到BridgeRouter合约。等待时间:取决于源链的区块确认时间。
  • 3)验证有交易发送到Home合约。等待时间:取决于源链的区块确认,当交易发送到BridgeRouter合约之后,应很快就能看到。此时没有办法来查询某笔特殊的交易,可交互检查BridgeRouter交易的时间戳。
  • 4)挑战期过后,验证有交易发送到目标链的Replica合约。可交叉检查时间戳。等待时间为挑战期,当前约为30分钟。
  • 5)验证有交易发送到目标链的BridgeRouter合约。等待时间为挑战期+区块确认时间。
  • 6)验证目标链的收款方收到了a token mint。等待时间为:源链的区块确认时间 + 挑战期 + 目标链的区块确认时间。

4. 以太坊端Optics成本分析

以以太坊为源链,发起token transfer:

  • 1)Updater向以太坊上的Home合约提交签名committedRoot的交易费约为53K,对应约为$7。【提交间隔可控制,无需每笔交易都提交一次。支持batch。】
  • 2)Relayer会向 除以太坊之外的其他各链(如Celo/Polygon等)的"以太坊Replica"合约转发相应已签名committedRoot。由于其他链交易费较低,可忽略。
  • 3)processor会向目标链上对应源链的Replica合约提交proveAndProcess请求,从而目标链上的接收方可收到相应的token。【目前,仅对于目标链为以太坊的情况,提交proveAndProcess请求的手续费由用户发起并承担。】

以 非以太坊为源链,发起token transfer:

  • 1)Updater 向非以太坊源链上的Home合约 提交签名CommittedRoot 的交易费可忽略。【提交间隔可控制,无需每笔交易都提交一次。支持batch。】
  • 2)Relayer 会向其他各链的“源链Replica”合约转发相应已签名committedRoot。此时,重点考虑向 以太坊上“源链Replica”合约提交 的gas为74K,对应约$9。【其他任何链发起转账请求,只要Updater提交了签名,都需要在以太坊上更新,会产生相应的成本。】
  • 3)以太坊上由用户自身发起proveAndProcess请求 的gas为178K,对应约$18。目标链为 非以太坊链 的话,由processor承担发起proveAndProcess请求相应的费用。

参考资料

[1] 2021年9月博客 Optics: How to Ape ERC-20 Tokens With Etherscan
[2] Optics Architecture
[3] Optics Token Bridge xApp
[4] Optics Developing Cross-Chain Applications
[5] Optics v2 deployment complete! Please help verify the deployment
[6] Optics v2 is live

你可能感兴趣的:(区块链,跨链)