Celo系列博客有:
前序博客有:
Optics Bridge开源代码见:
Optics中:
未来将针对NEAR和Solana实现Rust版本的链上合约。
Optics Bridge当前已部署在Celo、以太坊和Polygon主网上。
Celo、以太坊、Polygon主网上的Home及其它核心合约地址见:
Optics V2中,向以太坊上各Replica合约提交proveAndProcess
请求的费用需由用户承担,而不再由Optics社区运营的processor来承担:【Notice: Optics will no longer cover processing fees when returning to Ethereum.】【Cleo、Polygon上的prove/process
操作由相应的processor承担】
当前各链分配了唯一的Domian ID:
目前,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);
}
Optics会在链之间建立communication channels,但是这些channels由xApp(又名“cross-chain applications”) developers来使用。
在 https://github.com/celo-org/optics-monorepo 提供了标准的模式来集成Optics channels,来确保communication是安全的。
集成过程中,需要以下关键要素:
/**
* @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");
_;
}
.........
}
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合约:
Optics系统sketch为:
结果通常为二者之一:
尽管Optics的安全保证要弱于header-chain validation,但是,可满足大多数应用场景的要求。
Optics为一种新策略,可在不validate header的情况下,进行跨链通讯。
Optics的目标是:
该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个目的:
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中,必须:
通过在Router contract上实现以上功能,并部署在多条链上,从而可以通用语言和规则创建一个可用的跨链应用。这种跨链应用可将Optics作为跨链信使来相互发送和接收messages。
Optics团队 xApp Template 提供了xApp开发的模板,开发者可基于此实现自己的应用逻辑,并利用an Optics channel for cross-chain communication。
不同链上xApps之间Message Flow流程为:
为了实现a xApp,需定义跨链所需执行的actions,对于每个action类型:
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.
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);
}
}
Optics Token Bridge为a xApp,为Optics生态应用的一种,用于链之间的token transfer。
Token Bridge的主要特征为:保证token在多条链之间的流通总量保持不变。
部署在Celo、以太坊、Polygon主网上的Token Bridge合约地址见:
BridgeRouter、Home等合约地址有2套是因为,2021年11月26日,Celo团队对Optics协议进行了升级。
部署在以太坊上的XAppConnectionManager合约地址为:
部署在以太坊上的Replica合约有多个:
以太坊上wrapped ether合约为:
转账时的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流程可参看:
message处理规则为:
/**
* @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;
}
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");
}
}
Message派发规则为:
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);
}
send
函数接口。】
approve
来grant allowance for the tokens being sent to the local BridgeRouter contract。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
);
}
Message格式:
主要包括3大合约内容:
具体为:
1)BridgeRouter合约:
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;
}
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
);
}
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;
}
BridgeRouter
合约registry,使得:
BridgeRouter
合约消息。BridgeRouter
合约的消息2)TokenRegistry合约:
/**
* @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);
}
// store token in mappings
representationToCanonical[_token].domain = _tokenId.domain();
representationToCanonical[_token].id = _tokenId.id();
canonicalToRepresentation[_tokenId.keccak()] = _token;
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发送。
由链A将token发送到链B的message flow流程,主要参与方有:
详细流程为:
1)链A端
approve
相应数量的token到本链的BridgeRouter-A
合约。BridgeRouter-A
合约。
BridgeRouter-A
合约的资金池。BridgeRouter-A
合约会从用户钱包中burn相应数量的token。
BridgeRouter-A
token可burn non-native tokens,是因为相应的representation合约是由BridgeRouter-A
部署的,BridgeRouter-A
合约具有相应representation合约的administrator权限。BridgeRouter-A
为BridgeRouter-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());
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。
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,
})
}
committedRoot
,基于该committedRoot,从本地数据库中读取【???跟Updater共用一个数据库???而不是监听Home合约的Update(localDomain, _committedRoot, _newRoot, _signature)
事件??】已签名的update,调用Replica合约的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_process
或process
接口:【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端:
// 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)
}
onlyReplica
。onlyRemoteRouter(_origin, _sender)
。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 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());
}
}
Optics当前仍在迭代开发中。
Optics会将messages进行batch,仅发送tree roots,因此,一旦message被传入到Home合约,就无法在链上跟踪各个单独的message。可开发一个agent-querying工具,向链下agents query单独的每笔交易,当前并没有相应的工具存在。
因此,这意味着在send和receipt之间,存在a state of unknown,可将其看成是snail mail,尽管无法跟踪,但是由delivery confirmation。在链上可确认的事情仅有:
详细的message伪追踪流程为:
以以太坊为源链,发起token transfer:
以 非以太坊为源链,发起token transfer:
[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