ERC-6358 Cross-chain token state synchronization (Cross-Chain Token States Synchronization)
Review · Tokens · created 2023-01-17
A standard in which contracts deployed on multiple chains jointly record the same token ledger as if it were a shared copy. Instead of moving tokens to another chain, state is synchronized across all chains.
Why it is needed
Current token bridges move tokens from one chain to another, which causes a fragmentation problem where the same asset is split across different chains. Safety is an even more serious issue. If a user bridges Ethereum's ETH to another chain and that chain stops, the user loses that ETH. This standard chooses synchronization over transfer. Contracts on multiple chains record the same token state identically, and a trustless off-chain synchronization program reconciles the records between chains. This way, even if every other chain stops, as long as Ethereum keeps running, the user's assets do not disappear. The source explains that this approach solves the fragmentation problem and greatly increases the security of user assets across multiple chains.
What to implement
event TransactionSent(bytes pk, uint256 nonce);: An event emitted when an o-transaction with nonce `nonce`, signed by user `pk`, is submitted via a call to sendOmniverseTransaction.function sendOmniverseTransaction(ERC6358TransactionData calldata _data) external;: Submits an o-transaction. The signature, nonce, and payload must be verified, and actual execution must not happen inside this function.function getTransactionCount(bytes memory _pk) external view returns (uint256);: Returns the number of o-transactions sent by account `_pk`. This value becomes the valid nonce for a new o-transaction.function getTransactionData(bytes calldata _user, uint256 _nonce) external view returns (ERC6358TransactionData memory, uint256);: Returns the transaction data and timestamp corresponding to a specific nonce for an account.function getChainId() external view returns (uint32);: Returns the chain ID.function omniverseBalanceOf(bytes calldata _pk) external view returns (uint256);: In the fungible token extension, returns the balance of account `_pk`; in the NFT extension, returns the number of NFTs held by the account.function omniverseOwnerOf(uint256 _tokenId) external view returns (bytes memory);: Returns the omniverse account that owns the NFT corresponding to `tokenId`.
In depth
Omniverse Account (o-account)
This standard establishes a global user identifier shared across all chains, called an Omniverse Account (o-account for short). It is recommended that an o-account be represented by a public key generated with the secp256k1 elliptic curve. Since each chain has a different address scheme, an environment-specific mapping approach is recommended; the simplest implementation is to create two mappings, one from the secp256k1 public key to the account address in that environment, and its inverse. The source cites Flow's account system, which binds the public key to the account so the public key can be obtained directly from the account address, as a representative example.
The o-transaction data structure, ERC6358TransactionData
Every o-transaction must (MUST) be described using the ERC6358TransactionData struct. It has six fields. nonce is the sequence number of the o-transaction sent by the account; if the current nonce is k, the next valid nonce is k+1, and it must be defined as uint128 for compatibility with more blockchain technology stacks. chainId is the chain on which the o-transaction originated, as a uint32. initiateSC is the address of the contract where the transaction was first initiated, as bytes; from is the omniverse account that signed the transaction, as bytes; payload is bytes encoding developer-defined business logic data; and signature is bytes containing the signature over all the above information.
Payload encoding: Fungible and NonFungible
The payload contains the token operation. It is recommended to encode a Fungible struct (op uint8, exData bytes, amount uint256) for fungible tokens, and a NonFungible struct (op uint8, exData bytes, tokenId uint256) for NFTs. op indicates the operation type: values 0 through 31 are reserved, and 32 through 255 are custom. op 0 means the from account transfers amount (or tokenId, for NFTs) to the account specified in exData; op 1 means minting to the account in exData; op 2 means from burns its own tokens. exData holds the recipient account when op is 0 or 1, and is empty when op is 2.
Signature generation method
signature is the signature over the remaining information in the struct and must be defined as bytes. Two generation methods are presented. One concatenates the raw bytes of op, exData, and amount (or tokenId, for NFTs) to form the payload, then concatenates nonce, chainId, initiateSC, from, and payload and hashes the whole with keccak256. The other encapsulates the data as structured data according to EIP-712. Both are optional (OPTIONAL), and in both cases the final step is signing the hash value.
The IERC6358 interface and optional extensions
Contracts conforming to the standard must implement IERC6358. sendOmniverseTransaction must implement verification of the signature, nonce, and payload, and must emit a TransactionSent event when called. An important constraint is that this function is only for sending the o-transaction; execution must not (MUST NOT) happen inside this function, and it is recommended that actual execution take place in a different function after a certain delay. It is also recommended to maintain a map tracking the nonce for each account. The query functions are getTransactionCount, getTransactionData, and getChainId. Optional extensions include IERC6358Fungible (omniverseBalanceOf) for fungible tokens and IERC6358NonFungible (omniverseBalanceOf, omniverseOwnerOf) for NFTs, and these can also be implemented in combination with existing ERC-20 and ERC-721.
Global consistency reconciled through two nonces
There are two nonces. One is the nonce inside the o-transaction data, and the other is the account nonce that the ERC-6358 contract on each chain maintains per account. During synchronization, the two are compared for verification. When a user publishes an o-transaction with nonce k+1 on one chain, the account nonce on other chains is still k, so an off-chain synchronizer detects this discrepancy and relays the data. Synchronizers race to relay it because of the reward, and the reward strategy (fees, mining-style rewards, etc.) is determined by the token deployer. Once contracts on every chain verify the signature and execute after the waiting period ends, the account nonce becomes k+1 on all chains and the balance state matches. This guarantees the ultimate consistency of token state.
Architecture and proposal status
The source calls the ERC-6358 contracts deployed across multiple chains Abstract Nodes. The state recorded by the Abstract Node on each chain can be viewed as a copy of the global state, and they ultimately converge. The synchronizer is an off-chain program that carries o-transactions published by one chain's contract to other chains; since it only relays data bearing another user's signature, it is trustless. Note that this proposal's status is Review. It should be kept in mind that this is still at the review stage and its content may change.
Reference implementation
The source provides an example omniverse account expressed as a secp256k1 public key, along with its corresponding example private key. It also provides, under the assets path of the EIP repository, example implementations of the IERC6358, IERC6358Fungible, and IERC6358NonFungible interfaces, a common utility library (OmniverseProtocolHelper), and example implementations of ERC-6358 fungible tokens and NFTs. It states explicitly that ERC-6358 tokens can be used in combination with ERC-20 and ERC-721.
Things to watch out for
- Synchronizers cannot cheat: A synchronizer cannot generate another user's signature, so it cannot forge the o-transaction data it relays. Since it only receives a reward by submitting data containing a valid signature and amount, the source concludes that a synchronizer has neither the incentive nor the means to cheat.
- A user's double-spend attempt cannot succeed: A user could attempt a double-spend by submitting two different o-transactions with the same nonce to two chains. However, once synchronizers relay each side's data to the other chain, each chain's contract detects the misconduct based on the non-repudiable signatures. Because there is a fixed waiting period before execution, the attack cannot succeed. Even if a fraudulent transaction has already been executed due to a network failure, it can be rolled back from the point the fraud occurred, based on the nonce, and the attacker is penalized according to the token's design, such as through account lockout.