- 1. Introduction
- 2. File Structure
- 3. Get Started
- 4. Project configuration
- 5. Usage
- 6. Operation Scripts
- 7. Useful Commands
- 8. Generate Typescript Wrapper
- 9. Deploy and Setup Cross Chain Service
- 10. verify contracts
- 11. print necessary information of cross-chain service
- 12. release new version
- Issues
- License
This project is built for providing cross-chain service for Orderly V2, which has components including multiple vaults, a dedicated ledger, cross-chain managers for both the vaults and the ledger, and cross-relay for each chain with our services. These components are deployed across blockchains, such as Ethereum, Arbitrum, and Avalanche. The vaults serve as secure repositories for user funds, while the ledger acts as a comprehensive database for all user-related information. To facilitate seamless communication between the vaults and the ledger—each residing on different blockchains—we have implemented dedicated cross-chain managers. These managers are tasked with converting messages into cross-chain payloads, enabling fluid inter-blockchain communication. Recognizing the variety of existing cross-chain solutions, a relay is positioned on each blockchain to encapsulate multiple cross-chain options. This relay plays a key role in transmitting messages from the cross-chain managers, thereby ensuring robust and flexible cross-chain interactions
cross-chain-relay:
- receiving messages from cross-chain-managers
- sending messages received from the other chain to cross-chain-managers
- send message and payload through layerzero or other cross-chain method to the destination chain
Ledger cross-chain-manager:
- receiving message from cross-chain-relay and forward to ledger
- construct cross-chain message and payload and send then to cross-chain-relay
- convert messages sent from vault to ledger into the recognizeable type and forward to ledger
- convert token amount by using token decimal information for all message from and to vault on the other chain
Vault cross-chain-manager:
- receiving message from cross-chain-relay and forward to vault
- construct cross-chain message and payload and send then to cross-chain-relay
- convert messages sent from ledger on the other chain to vault into the recognizeable type and forward to vault
Diagram:
-
The vault receives a user's deposit request.
-
The deposit message is sent to the Vault Cross-Chain Manager.
-
The manager constructs a cross-chain message including source and target chain information.
-
This message is sent to the Relay on the source chain.
-
The Relay transmits the message to the target chain, where the Ledger is deployed.
-
The Relay on the target chain forwards the message to the Ledger Cross-Chain Manager.
-
This manager decodes the message and constructs a recognizable message for the Ledger.
-
Finally, the Ledger processes the deposit.
Here's an overview of the main folders in this project and what they contain:
-
contracts/
: This folder houses all the Solidity smart contracts essential for the project's blockchain functionality. -
script/
: Contains the base classes for various scripts along with utility functions and helpers. -
foundry_ts/
: This directory stores typescript wrapper for foundry scripts, so that foundry scripts can be better utilized. -
config/
: A general folder for storing project-related informations like contract address.
To get started, you need to install the following dependencies and prepare some pre-settings:
- nodejs, yarn
- foundry
- follow the Project configuration section to setup your project configuration
- install deps: yarn install
- build: forge build
- test: forge test
First of all, I will introduce the role of json files under config
. Foundry scripts are not like javascript or ts. It is not that convenient to save deployment infos and projecte related infos into files. So we usually copy and paste the info into some place(confluence, or readme). But copy paste isn't a good habit. So I decide to put everything into json files automatically. So I write a helper base class for foundry scripts so that it can help users better read from and write to json files.
The json files under config
is organized by env
first, and then network
, and then specific infos. Just like the following relay deployment example:
"staging": {
"orderlyop": {
"owner": "",
"proxy": "",
"relay": ""
},
"arbitrumgoerli": {
"owner": "",
"proxy": "",
"relay": ""
}
}
The above example stores cross-chain-relay service's deployment address and owner information for staging environment. Because we are developing under a certain env, it would be better if we separate infos of different envs.
Other project related json files are oragized by the same way. The json file read and write helper foundry script is baseScripts/ConfigHelper.s.sol
it wraps the read and write of cross-chain-relay infos and cross-chain-manager infos, and other useful helper functions.
Beside json files under config
, other public informations are stored in .env
, and the example is .env.example
.
in .env
you can set your private keys, chain RPC URLs, chain Ids and other public information.
-
Setup available accounts(format like this:
ORDERLY_PRIVATE_KEY
, network name with_PRIVATE_KEY
) -
Setup RPC URLs
-
Setup Layerzero Endpoins addresses
In this section, I will introduce how to deploy evm-cross-chain service for orderly v2, and how to setup every contracts. Scripts organization and related file format will also be introduced.
You need prepare your .env
file first. For every network you want to deploy, you need to set the following variables in .env
.
for private key configuration
- set XXX_PRIVATE_KEY in .env
for contract verification
- set etherscan key(if needed) and explorer api url in
.env
(for contract verification)
for basic network configuration
- set XXX_CHAIN_ID in .env
- set XXX_RPC_URL in .env
- set XXX_LZ_CHAIN_ID in
.env
(for layerzero cross-chain support) - set XXX_ENDPOINT in
.env
(for layerzero cross-chain support)
This detailed steps of deployment is described in section 9. Deploy and Setup Cross Chain Service. Here I will introduce the steps in a more general way.
- deploy the implementation contract
- deploy proxy contract
- initialize the contract
After deployment, you need to setup the contracts. Cross-chain-relay and cross-chain-manager has different setup procedures. for cross-chain-relay, please follow the following setup procedure:
- set the current chain Id
- transfer native token to the proxy address, for future cross-chain consumption
- set native chain Ids to layerzero chain Ids mapping
- set the corresponding cross-chain-manager's address
- set layerzero trusted remote
for ledger cross-chain-manager, the setup procedure is like:
- set the the current chain Id
- set the cross-chain-relay's address
- set Ledger address
- set token decimal information for different chains
for vault cross-chain-manager, the setup procedure is like:
- set the the current chain Id
- set the cross-chain-relay's address
- set Vault address
- set the ledger cross-chain-manager's network chain Id and the the address
Some components relies on other components, so you'd better deploy all of them so before your setup starts.
There are some times we need to add new vault chains. So we need to deploy cross-chain-relay and cross-chain-manager on the new added vault chain. And update settings of contracts on the ledger side. The following steps are the procedures of adding a new vault chain.
- update token decimal in
config/token-decimals.json
, make sure the new added vault chain's token decimal is set correctly. - update project related infos in
config/project-related.json
(or you can update it later, if you choose to setup vault later, you should drop the --connectVault flag in the following script) - run a script to add a new cross-chain service on the new added vault chain and setup it and update ledger side contracts settings
ts-node foundry_ts/entry.ts --method addVaultCCService --env dev --vaultNetwork opgoerli --ledgerNetwork orderlyop --initEther 0.01 --broadcast --connectVault
If you didn't transfer enough token to the proxy address, you can run the following script to transfer more token to the proxy address:
ts-node foundry_ts/entry.ts --method transferNativeToken --network <network> --to <address> --ether <amount> --broadcast
If you didn't connect vault cross-chain-manager to vault in the above script, you can run the following script to connect them:
ts-node foundry_ts/entry.ts --method setCCManagerVault --env <env> --network <network> --broadcast
If you add new operations in cross-chain-relay or cross-chain-manager, you should also setup config/cross-chain-method-gas.json
, and call the following script on every chain:
ts-node foundry_ts/entry.ts --method setCrossChainFeeAll --env <env> --network <network> --broadcast
- add git tag and push to github
- update abi json file in contract-abi
- fill the information on confluence Orderly V2 Contract Information Board
- fill the balance monitor information to make sure enough balance on the new added vault cross-chain relay.
To better manage the complex procedures of cross-chain-relay and cross-chain-manager, we define the single operation of each step in above procedures as operation
. Each operation is realized using a foundry script and a typescript wrapper. And some useful composite operations(includes several operations together) can be built using typescript wrapper (call several operations in one invocation).
Before we start on introducing the operation script structure. We need to introduce some helper scripts first. Because cross-chain-relay and cross-chain-manager has fixed procedures, so we don't need write a same operation again and again. Therefore, I provide some helper function in scripts to help users better build more other operations. For example, cross-chain-relay's helper script locates at ./script/baseScripts/RelayHelper.s.sol
, it implements all operations for cross-chain-relay. Cross-chain-manager's helper script locates at ./script/baseScripts/CCManagerHelper.s.sol
.
Now let's take a look at the structure of the actual operation scripts.
First an operation implemented by a foundry script under ./script/foundry_scripts
. The file structure under ./script/foundry_scripts
depends on the role of the operations, relay's operations are placed under relay
, cross-chain-manager's operations are placed under ccmanager
. General operations like retryPayload
, transferNativeToken
is place under ./script/foundry_scripts
.
Every foundry script is wrapped by a typescript script. The required input arguments of foundry scripts are passed from typescript script's arguments. The arguments will be formatted as FS_${operation_name}_${argument_name}
and set in .env
by typescript, so that foundry script can read them by using vm.env
. Let's take a look at an example, to see how a script is invoked.
To deploy cross-chain-relay, we first write a foundry script and inherit necessary base contracts and helper scripts.
contract DeployRelay is BaseScript, ConfigHelper, RelayHelper {...}
deploy a cross-chain-relay, you need to know what current env is and which network to deploy. So, you need env
and network
, let we name this operation as deployRelay
. Then the variables set int .env
will be FS_deployRelay_env
and FS_deployRelay_network
.
You need to read these arguments from .env
and deploy cross-chain-relay.
But manually set values in .env
is not a wise choice, so we let the typescript wrapper to do that (every foundry scripts has a typescript wrapper).
export function deployRelay(
env: string,
network: string,
broadcast: boolean,
simulate: boolean
) {
setupDeployJson(relay_deploy_json, env, network, "relay");
set_env_var(method_name, "env", env);
set_env_var(method_name, "network", network);
set_env_var(method_name, "broadcast", broadcast.toString());
foundry_wrapper(method_name, broadcast, simulate);
}
The above function is from the typescript wrapper. It first setup the json file for storing the deployment infos (as we explained in previous section, we let the foundry script automatically save the deployment infos, but because of the limitation of foundry script, we setup it using typescript first).
Then it setup .env
for foundry script to read. Finally it invokes the foundry script by calling foundry wrapper function.
This function is from ./foundry_ts/methods/relay/deployRelay.ts
.
after writing the above function, we need also register the operation, so that we can use a unified command line scripts to invoke operations.
export function deployRelayWithArgv(argv: any) {
const required_flags = ["env", "network"];
checkArgs(method_name, argv, required_flags);
deployRelay(argv.env, argv.network, argv.broadcast, argv.simulate);
}
addOperation(method_name, deployRelayWithArgv);
the first function checks the required arguments are passed and then invoke the operation. And then calling addOperation
to register the operation.
After finishing above programming, we can call deployRelay
using command line:
ts-node foundry_ts/entry.ts --method deployRelay --env qa --network eth --broadcast --simulate
you may notice there are two more arguments --broadcast
and --simulate
, if --broadcast
is present, then the tx will be broadcast, if --simulate
is present then the foundry script will not get executed, it will only print the command that invoke the foundry script.
(PS: because we are using minimum argument parser, we have little limitation on the type of arugment, so if you want to read bytes, you should define your argument name as bytes
, because it is manually set as string in the script, or else hex strings will be parsed as integers.)
To add a new script, 1. you need to write a foundry script first, and then 2. add a typescript wrapper and 3. register the operation into the script.
Of course you can start will existing typescript operations, and just add a new typescript operation by calling several existing typescript operations. In this way, you are building more complex operations and don't need to write a foundry script. But you also have to register it.
The second way of adding new script using typescript is recommended. Foundry script should be regarded as low-level invocation of contract operation. Complex operations that are more useful in real situation should be implemented in typescript.
here is a sample command line:
ts-node foundry_ts/entry.ts --method setCrossChainFee --ccmethod pingPong --fee 500000 --env dev --network orderlyop --broadcast
ts-node foundry_ts/entry.ts --method retryPayload --env staging --network arbitrumgoerli --data <0x> --broadcast
ts-node foundry_ts/entry.ts --method upgradeCCManager --env dev --network arbitrumgoerli --role vault --broadcast
change the env
network
role
into the correct value according to your goal.
I implement a code generator for foundry script wrapper. Simply run:
ts-node foundry_ts/genCode.ts --role relay --sol foundry_scripts/relay/sendPingPong.s.sol
pass the role(relay or ccmanager) and the path of the foundry script, then the foundry wrapper script will be generated.
make sure that you set your method name same with the foundry script file name. Which mean in the above example, you should read your arguments as FS_sendPingPong_xxx
from env.
before you start deployment, you should have several variables set up in .env
:
- network private key
- network chain id
- network rpc url
- network layerzero chain id
- network layerzero endpoint address
set vault ledger and operator address in config/project-related.json
set token decimal information in config/token-decimals.json
we have a integrated script for deploying cross-chain relay and cross-chain manager. you can run the following command to deploy and setup cross-chain service:
ts-node foundry_ts/entry.ts --method deployAndSetupAnEnv --env production --vaultNetwork arbitrum --ledgerNetwork orderlymain --initEther 1 --broadcast
--initEther
has no effect right now. We require you manually transfer native later after this script.
to verify your contracts on blockscout, you should run the following command:
forge verify-contract <address> <contract-path>:<contract-name> --chain-id <chain-id> --verifier-url https://testnet-explorer.orderly.org/api\? --verifier blockscout
or you can verify using the following script. It will automatically find the address of contract and verify it on blockscout.
ts-node foundry_ts/entry.ts --method verifyContract --contract <CCRelay|VaultCCManager|LedgerCCManager> --network <network> --env <env>
this example shows how you can verify the contract on orderly L2 chain. The reason for the \?
suffix of the verifier-url is discussed on a github issue: foundry-rs/foundry#5160.
here is a sample command:
forge verify-contract <contract-address> contracts/CrossChainRelayUpgradeable.sol:CrossChainRelayUpgradeable --chain-id 421613 --verifier-url https://api-goerli.arbiscan.io/api -e <etherscan-api-key>
or you can verify using the following script. It will automatically find the address of contract and verify it on etherscan like explorers:
ts-node foundry_ts/entry.ts --method verifyContract --contract VaultCCManager --network arbitrum --env production
the example above shows how you can verify contract arbitrum goerli network, whose explorer api is: https://api-goerli.arbiscan.io/api
. Because it uses infrastructure the same as etherscan, so we can use the same verification way to verify contracts on arbitrum goerli. arbitrum-goerli's etherscan api key is shared with arbitrum's mainnet. So, you can generate an api key using the mainnet explorer, cause' arbitrum-goerli's explorer has no where to do that.
verifing contracts using forge verfiy-contract
sometimes may not work. It will print OK
, but if you check the explorer, you will find the contract is not verified. It could possibly be the wrong constructor arguments or the compiler version. So you have to pass --constructor-args
with abi encoded constructor arguments and --compiler-version
with the compiler version used to compile the contract. Or you can output the standard json input or the flattened contract souce and verify it on explorer manually.
- generate standard json input
forge verify-contract --chain-id <id> --verifier-url <url> -e <key> --show-standard-json-input <address> <contract> > standard.json
- generate flattened solidity file
forge flatten <contract> > flattened.sol
then you can verify it on explorer using the generated standard json input or flattened solidity file. Make sure you set the constructor arguments correctly. You can goto https://abi.hashex.org/
to get the abi encoded constructor arguments.
simple run the following command:
print cross-chain relay information on chain:
ts-node foundry_ts/entry.ts --method printRelay --env production --network arbitrum --dstNetwork orderlymain
print cross-chain manager vault side information on chain:
ts-node foundry_ts/entry.ts --method printCCManagerVault --env production --network arbitru
print cross-chain manager ledger side information on chain:
ts-node foundry_ts/entry.ts --method printCCManagerLedger --env production --network orderlymain
print token decimal configuration on chain:
ts-node foundry_ts/entry.ts --method printCCManagerTokenDecimal --env production --network orderlymain
you need to change the parameters to suit your needs.
To release a new version on dev, qa, staging or production, you need to follow the following steps:
- make sure features are all complete
- make sure you write tests on those features, and tests passed
- copy the new abi to abi folder
- deploy and setup the new version on dev, qa, staging or production
- add git tag and push to github
-
if you put urls like https://testnet-explorer.orderly.org/api\? into
.env
file. foundry script will have problem parsing.env
. you need to use "" to enclose it and add\
to escape the parsing. so it will be like: "https://testnet-explorer.orderly.org/api\\\\?" -
Oracle backoff: In a certain situation, oracle could be backoff for a long time, which will block relayer for a long time. Then txs could be in pending. The cause of oracle backoff is Orderly chain stopping mining.