LayerZero ODCs
The Omnichain implementation of the ODC brings multi-chain communication capabilities to the Nexera Standard, enabling read/write
operations for components distributed on different blockchains. The ODC components rely on the ODC Omnichain Proxy that routes requests to the LayerZero Endpoint infractrucutre.
Interaction Process
The Omnichain Proxy is a LayerZero OApp which provides essential functionality to communicate with the LayerZero Endpoint. The Omnichain Proxy is used to:
- Manage cross-chain Data Point access.
- Enable communication between Data Objects located in different blockchains.
Another component that plays an important role in the cross-chain communication process is the Omnichain Data Index smart contract; it allows:
- Managing access to Data Points for both local and remote Data Managers, with the Data Index using the Omnichain Proxy to verify admin rights across blockchains.
- Ensuring trustworthy requests from other blockchains via the Omnichain Proxy, which serves as a trusted party providing valid origin data to the Data Index.
Note
The Omnichain Data Index is based on the EIP-2535 standard and its functionality depends on the facets selected for its implementation.
Below you can find the interaction flow for the LayerZero ODC implementation:
Scenario Type | Flow |
---|---|
Write to Local and Remote Data Objects | User -> Local Data Manager -> Local Data Index -> Local Data Object -> Local ODC Proxy -> Local LZ Endpoint -> Remote LZ Endpoint -> Remote ODC Proxy -> Remote Data Index -> Remote Data Object |
Note
Developers have the flexibility to deploy their own or reuse someone else's Data Objects across different blockchains, which can communicate with each other through the Omnichain Proxy.
The diagram below illustrates the process of cross-chain interaction between different ODC components.
Context: A User wants to allocate a new data point on Chain 1 and grant approval for the Data Manager on Chain 2 to use this data point.
Click here to see the detailed description of the flow.
-
The User allocates a new data point via the Data Point Registry on Chain 1.
-
The User then sends a request to the Data Index to approve the Data Manager on Chain 2 to access the data point allocated on Chain 1. This request verifies whether the User is the Admin of the data point on Chain 1.
-
The Data Index sends the request to the Omnichain Proxy, which forwards the request to the LayerZero Endpoint.
3.1 The LayerZero Endpoint, serving as a gateway between the two blockchains, relays the request to the corresponding LayerZero Endpoint on Chain 1. This request is sent to Chain 1 as a transaction using the LayerZero off-chain service.
-
The LayerZero Endpoint on Chain 1 forwards the request to the Omnichain Proxy on Chain 1.
4.1 The Omnichain Proxy forwards the request further to the Data Index.
4.2 The request is then sent to the Data Point Registry, which verifies that the User is indeed the Admin of the data point.
4.3 The Data Point Registry returns the result of the operation to the Data Index.
4.4 The Data Index forwards the operation result back to the Omnichain Proxy.
4.5 The Omnichain Proxy forwards the operation result back to the LayerZero Endpoint, which sends the operation result in a separate transaction, passing the data to the next endpoint on Chain 2.
-
The LayerZero Endpoint on Chain 2 returns the operation result to the Omnichain Proxy.
5.1 The Omnichain Proxy forwards the operation result to the Data Index, which ultimately grants the Data Manager access to the data point from Chain 1.
Cross-chain Asset Transfer
LayerZero ODCs can be utilized for a variety of use cases that involve performing actions across different blockchains using the ERC-7208 standard. The example provided below demonstrates the fractionalization of an asset and transfer use case and how it can be implemented in your dapp system.
The omnichain communication enables the asset transfer between Chain 1 and Chain 2. In this cross-chain transfer example, a user first wraps their asset (ERC-20/721/1155) in the Vault using the OmnichainWrapperFractionalizerDM
, then retrieves ERC-1155 and ERC-20 fractions. These fractions are then transferred to Chain 2 via the Omnichain proxy and the LayerZero Endpoint.
The diagram below highlights the process of asset wrapping, fractionalization, and transfer to Chain 2:
Click here to see the detailed description of the flow.
-
The User sends the request to the
OmnichainWrapperFractionalizerDM
smart contract to wrap their assets.1.1 The
OmnichainWrapperFractionalizerDM
smart contract wraps assets in the Vault.1.2 The
OmnichainWrapperFractionalizerDM
sends the request to theOmnichainFungibleFractionsDO
smart contract through the Data Index to mint new fractions. The Data Index acts as a gating mechanism to check necessary permissions forOmnichainWrapperFractionalizerDM
.1.3 The Data Index forwards the mint request to the
OmnichainFungibleFractionsDO
, which mints the fractions.1.4 The User receives both ERC-1155 and ERC-20 fractions at their address.
-
The User initiates the transfer of fractions to Chain 2 and sends the request to the
OmnichainWrapperFractionalizerDM
.2.1 The
OmnichainWrapperFractionalizerDM
sends the transfer request to theOmnichainFungibleFractionsDO
via the Data Index, which checks if it has sufficient write permissions.2.2 The Data Index forwards the transfer request to the OmnichainFungibleFractionsDO.
2.3 The OmnichainFungibleFractionsDO decreases the User balance in the Chain 1 and sends the request to the Chain 2 to increase the fraction balance. The request is sent to the Omnichain Proxy.
2.4 The Omnichain Proxy forwards the increase balance request to the LayerZero Endpoint. It triggers the LayerZero offchain service to create a new transaction on Chain 2 to finalize the fraction transfer process.
-
The LayerZero Endpoint sends the transfer request to the Omnichain Proxy on Chain 2.
3.1 The Omnichain Proxy then forwards the transfer request to the Data Index, which checks for sufficient write permissions.
3.2 The Data Index forwards the transfer request to the
OmnichainFungibleFractionsDO
, which finally increases the fraction balance on Chain 2.3.3. To make the balance update visible to offchain services, the
OmnichainFungibleFractionsDO
sends the request toOmnichainWrapperFractionalizerDM
to emit theTransfer
event.
Note
There are situations when a transaction originating from Chain 1 gets reverted on Chain 2 due to different reasons. A developer must implement an error-handling mechanism in the Data Object to handle these scenarios, because usually, the revert will not be visible to the Data Object located in Chain 1.
ERC-20 Omnichain Transfer Example
The ERC-20 Fraction transfer involves function calls across multiple contracts. This section will describe the diagram above from the perspective of the function execution flow, dividing it into four parts, each representing the boundary of the corresponding component.
Chain 1
Data Manager
The flow begins with a user interacting with the Data Manager by calling the transferFrom
function presented in the FractionERC20DataManager
contract. This function acts as the entry point for the user to make an omnichain transfer:
The FractionERC20DataManager
inherits from OmnichainUpgradeableERC20DataManager
, which also inherits from the OmnichainERC20Transfers
contract, where the transferFrom
function is declared.
The transferFrom
function returns the result of the invocation of the _transferFromOmnichain
function:
As you can see, inside the code, the _writeOmnichainTransfer
function is invoked, which is declared in the OmnichainUpgradeableERC20DataManager
contract:
Inside the function, you can see the invocation of the dataIndex.write()
function, which forwards the transfer message to the Omnichain Data Index implementation. This step marks the transition of the execution flow to the Data Index component.
Data Index
The write()
function is declared under the DataManagerFacet
, which represents the Data Index:
Once the message is delivered to the Data Index, it calls the onlyApprovedDM()
function to verify if the caller (Data Manager) has sufficient permissions. Only then does it call the write()
operation on the Data Object, forwarding the message data further.
Data Object
The contract that receives the message from the Data Index is the OmnichainFungibleTokenDO
. This contract inherits from the OmnichainBaseDataObject
, which further inherits from BaseDataObject
. It has the write()
function declared, which was initially called by the Data Index:
This invocation calls the _dispatchWrite
function, which is declared under the OmnichainBaseDataObject
contract. The _dispatchWrite
function relies on the IOmnichainFungibleTokenOperations.omnichainTransfer.selector
to make the _omnichainTransfer
call:
Before the omnichain transfer is made, the account balance is decreased on the local chain by calling _decreaseBalance
, and _increaseOmnichainBalance
is called on the target chain. The latter function is declared under the OmnichainFungibleTokensDO
contract. Inside the _increaseOmnichainBalance
function, the _sendIncreaseOmnichainBalanceRequest
is called, which is also part of the OmnichainFungibleTokensDO
contract:
Note that this function forwards the request to the Omnichain Proxy by calling queryDataObjectWrite
. This call relies on the IOmnichainFungibleTokenOperations.omnichainIncreaseBalance.selector
to execute the _omnichainIncreaseBalance
function.
Omnichain Proxy
The queryDataObjectWrite
function is part of the OmnichainProxy
contract:
During the execution, _sendMessageAndRecordCallback
is called, which subsequently calls the _sendMessage
function:
The final step in this function execution chain is the _lzSend()
function, which ultimately sends the request to the LayerZero Endpoint and transfers the ERC-20 fractions to Chain 2.
Chain 2
Omnichain Proxy
Once the message is delivered to the Omnichain Proxy on Chain 2, the _lzReceive()
function is invoked, and in the process, the second function _executeReceivedQuery()
is called, which is responsible for processing the request from Chain 1:
The _executeReceivedQuery()
function contains the invocation of the _executeOperation()
which is responsible for determining which operation to invoke: check the admin of a data point, approve a Data Manager, read from a Data Object, or write to a Data Object. In this example, the operation writes to the Data Object - _executeDataObjectWrite()
:
In the _executeDataObjectWrite()
operation, the Data Index writeOmnichain()
function is called:
Data Index
The writeOmnichain()
function is declared under the OmnichainDataFacet
contract. It verifies if the caller is the trusted Omnichain Proxy implementation, allowing trust in the originalSender
account, meaning that it is the address that in fact initially sent this message from Chain 1.
Then the write()
operation is called on the Data Object located on Chain 2:
Data Object
The contract that receives the message from the Data Index is the OmnichainFungibleTokenDO
. This contract inherits from the OmnichainBaseDataObject
, which further inherits from BaseDataObject
. It has the write()
function declared, which was initially called by the Data Index:
The _dispatchWrite()
function is conditioned by IOmnichainFungibleTokenOperations.omnichainIncreaseBalance.selector
to call the
_omnichainIncreaseBalance()
function.
The ERC-20 standard requires emitting a Transfer
event after the increase balance operation. For this, the callIncreaseBalanceHandlersAndRevertOnFail()
handler is called in the _omnichainIncreaseBalance()
function. It ensures that the Data Manager emits the event.
Data Manager
By calling the callIncreaseBalanceHandlersAndRevertOnFail()
handler, the underlying function of the Data Manager is called: IOmnichainFungibleTokenIncreaseBalanceCallback(handler).afterOmnichainFungibleTokenIncreaseBalanceOnTargetChain()
.
It verifies that the caller of the function is indeed the Data Object address and emits the Transfer
event.
If this call reverts, the balance is decreased.
The result of this operation is returned to the initial caller, which is the Omnichain Proxy
.
Omnichain Proxy Callback
Chain 2
The result of the increase balance operation is returned to the _executeReceivedQuery()
function in the Omnichain Proxy
, and the callback is triggered.
The callback data is sent as a transaction to Chain 1 via the LayerZero Endpoint. Specifically for this, the _lzSend()
is called and the Operation.CALLBACK
is requested on Chain 1.
Chain 1
Omnichain Proxy
Once the message reaches the Omnichain Proxy on Chain 1, the _lzReceive
function is called:
Then the _executeCallback()
function is called, which forwards the succeeded/failed result back to the Data Object via the
IOmnichainCallbackReceiver(cbd.target).omnichainCallback(guid, cbData)
call:
The origin of the result is tracked and determined by the Data Object via the guid
identifier. Note that the guid
argument is identified in the Data Object as the rid
.
Data Object
When the operation result reaches the Data Object, the omnichainCallback()
function verifies if the callback data sender is indeed the Omnichain Proxy
, and the next operation, _omnichainCallback
, is invoked:
The _omnichainCallback
function acts in two ways:
- If the selector is correct, the
_omnichainIncreaseBalanceCallback
is called. - Filters nonexistent contract calls via the
UnknownWriteOperation()
function.
If the transfer to Chain 2 was not successful, then the refund is issued on Chain 1 via the
_refundOmnichainTransfer()
call. By invoking this operation, the balance is increased for the refund address
specified as an argument in the transferFrom()
function: