If you are looking to review this repository, please read the auditor's guide first.
If bridge is going to work with already deployed stablecoins on destination chain, go to section 3 of respective stablecoins.
If you wish to test an existing bridge, go to section 4 of respective stablecoins.
- Foundry
- python3
- jq
- yq
For macOS, python3 is installed by default, and jq and yq can be installed via Homebrew:
brew install jq yqInstall the dependencies for the contracts and scripts in this repository:
forge install
Circle's USDC deployment scripts may require a different node and yarn versions than the ones installed by default on your system. It is recommended to use nvm to set the node version, and yarn set version to set the yarn version. The required versions are:
node: 20.18.0yarn: 1.22.19
- Copy
.env.exampleto.envin the root directory of this repository. Set aPASSWORDandACCOUNT_NAMEfor the deploy address. The private key for the deployer address will be encrypted with this password. Set theNETWORKdepending on if this is a mainnet or testnet deployment.
cp .env.example .env
- Create an account that will function as the deployer address, and set the private key of that account through Foundry keystore:
source .env
cast wallet import $ACCOUNT_NAME --unsafe-password $PASSWORD --interactive
-
Fund this address with some native assets on both
srcanddestchains to pay for gas. Also fund it with USDC and USDT onsrcchain if you are going to test the bridge. 1 cent of each stablecoin is enough for testing. -
Set
SRC_VERIFIER_API_KEYandDEST_VERIFIER_API_KEYif there is an API key for verification in the chain explorer used for the respective chain. You can remove the relevant variable if an API key is not necessary for verification.
Note
Verification logic uses Foundry underneath, see forge verify-contract -h for the possible verifier (in config.toml) options. etherscan type is remapped to custom to standardize the commands being run.
Note
Throughout the codebase, all upgradeable contract deployments and initializations are done atomically except the USDC token deployment as Circle's own scripts are used for that purpose. This means a malicious actor can frontrun the USDC initialization transaction, which would lead to the USDC deployment script reverting during the initialization stage. If that happens, discard that deployment and consider using a private mempool for the re-deployment. As Circle's script atomically sets the implementation during the proxy deployment, the process is not susceptible to the CPIMP attack, griefing is the only possible impact.
-
Fill all
[*.usdc.*]fields except the ones ending with.deploymentinconfig/<mainnet or testnet>/config.toml. -
Run the deployment commands:
make usdc-and-bridge
-
Verify that
makeexited successfully, you should see a message with the ✅ emoji if all steps are completed without errors. -
Save the USDC compilation output for verification. Copy the compilation outputs of the relevant contracts to this repository if canonical deployment. This step is not critical since the deployment of USDC is done through official Circle scripts, and Blockscout can automatically verify the contracts due to bytecode equivalence. You can find the compilation outputs in the created
stablecoin-evmdirectory under the root level of this repository. Copyingbroadcast/deploy-fiat-token.s.sol/<your-chain-id>/run-latest.jsonand theartifacts/foundrydirectory is a good practice in case something goes wrong later. -
Go to section 4 to test the bridge.
- Run the USDC deployment script after filling
[dest.usdc.init]:
make usdc-deploy
-
Verify that
makeexited successfully, you should see a message with the ✅ emoji if all steps are completed without errors. -
Save the compilation output for verification. Copy the compilation outputs of the relevant contracts to this repository if canonical deployment. This step is not critical since the deployment of USDC is done through official Circle scripts, and Blockscout can automatically verify the contracts due to bytecode equivalence. You can find the compilation outputs in the created
stablecoin-evmdirectory under the root level of this repository. Copyingbroadcast/deploy-fiat-token.s.sol/<your-chain-id>/run-latest.jsonand theartifacts/foundrydirectory is a good practice in case something goes wrong later.
-
Fill the fields of
[dest.usdc.bridge.init]and[src.usdc.bridge.init]inconfig/<mainnet or testnet>/config.toml. -
Run the bridge deployment script:
make usdc-bridge-full
- Verify that
makeexited successfully, you should see a message with the ✅ emoji if all steps are completed without errors.
- Test the deployment by running the test script which sends 1 cent from source chain to destination chain, you need to have some USDC on source chain for this:
make usdc-bridge-mint-test
-
If the script is successful, search for the
sendtransaction (the other one is approval) in the output of the script on LayerZero Scan. Wait for the destination transaction hash, look it up on destination chain explorer, and confirm that destination chain USDC was minted to the address associated with the private key used above. -
Similarly, run the test script which sends 1 cent from destination chain to source chain, you need to have some bridged USDC on destination chain for this:
make usdc-bridge-burn-test
- If the script is successful, search for the
sendtransaction (the other one is approval) in the output of the script on LayerZero Scan. Wait for the destination transaction hash, look it up on source chain explorer, and confirm that USDC was burned from destination chain and 1 cent was sent to the address associated with the private key used above.
- Upgrade the USDC bridge contracts to the Circle takeover version by running the upgrade script from respective proxy admin owners:
forge script ./script/usdc/for_circle_takeover/01_USDCSrcBridgePrepareTakeover.s.sol --private-key <SRC_USDC_BRIDGE_PROXY_ADMIN_OWNER_PRIVATE_KEY> --broadcast
forge script ./script/usdc/for_circle_takeover/02_USDCDestBridgePrepareTakeover.s.sol --private-key <DEST_USDC_BRIDGE_PROXY_ADMIN_OWNER_PRIVATE_KEY> --broadcast
- Set the
BlockedMsgLibas the send library of both ends of the bridge, should be called by respective bridge owners:
SRC_LZ_BLOCKED_MSG_LIB=<BLOCKED_MSG_LIB_ON_SRC> forge script ./script/usdc/for_circle_takeover/03_USDCSrcBridgeSetBlockedMsgLib.s.sol --private-key <SRC_USDC_BRIDGE_OWNER_PRIVATE_KEY> --broadcast
DEST_LZ_BLOCKED_MSG_LIB=<BLOCKED_MSG_LIB_ON_DEST> forge script ./script/usdc/for_circle_takeover/04_USDCDestBridgeSetBlockedMsgLib.s.sol --private-key <DEST_USDC_BRIDGE_OWNER_PRIVATE_KEY> --broadcast
This is done to prevent messages being sent out so that they do not get stuck after the bridge is paused in the next step. Wait for a while after this step and ensure there are no messages inflight by checking LayerZero Scan before proceeding with pausing the bridge.
- Pause both ends of the bridge, should be called by respective bridge owners:
forge script ./script/usdc/for_circle_takeover/05_USDCSrcBridgePause.s.sol --private-key <SRC_USDC_BRIDGE_OWNER_PRIVATE_KEY> --broadcast --ffi
forge script ./script/usdc/for_circle_takeover/06_USDCDestBridgePause.s.sol --private-key <DEST_USDC_BRIDGE_OWNER_PRIVATE_KEY> --broadcast --ffi
Note
Both of these scripts need the ffi flag as they check if there any inflight messages by running InflightMsgCheckLzScan.sh before pausing the bridges. If you want to force pausing even though there are messages with INFLIGHT or CONFIRMING status, simply comment out _checkInflightMessages() line and re-run the scripts.
- Remove bridge's minter role from destination USDC, should be called by
MasterMinter's owner:
forge script ./script/usdc/for_circle_takeover/07_USDCRemoveBridgeAsMinter.s.sol --private-key <MASTER_MINTER_OWNER_ADDRESS_PRIVATE_KEY> --broadcast
- Set Circle's address so they can perform the USDC burn action on source chain end of the bridge. This script also sets the destination USDC total supply by reading from the destination chain so that Circle can burn the correct amount of USDC on source chain. This script should be called by the source USDC bridge owner:
SRC_BRIDGE_CIRCLE_ADDRESS=<ADDRESS_GIVEN_BY_CIRCLE> forge script ./script/usdc/for_circle_takeover/08_USDCSrcBridgeSetCircleAndDestSupply.s.sol --private-key <SRC_USDC_BRIDGE_OWNER_PRIVATE_KEY> --broadcast
Warning
Make sure reported destination USDC total supply in logs of the above script matches with the actual value by checking it on destination chain explorer. If the value is incorrect, do not proceed with the next steps and investigate the issue. In this script, total supply of USDC on destination chain is read directly from destination RPC. If RPC is not reliable, you may consider deploying a cross-chain reader contract utilizing lzRead and use that contract as the destUSDCSupplySetter instead.
- Transfer the proxy admin of USDC to Circle's given address:
CIRCLE_USDC_PROXY_ADMIN=<ADDRESS_GIVEN_BY_CIRCLE> forge script ./script/usdc/for_circle_takeover/09_USDCProxyAdminTransfer.s.sol --private-key <DEST_USDC_BRIDGE_PROXY_ADMIN_OWNER_PRIVATE_KEY> --broadcast
- Transfer USDC's ownership to a contract that Circle can later use to retrieve the ownership of USDC,
USDC_ROLES_HOLDER_OWNERshould be in our control:
USDC_ROLES_HOLDER_OWNER=<OWNER_ADDRESS> forge script ./script/usdc/for_circle_takeover/10_USDCTransferOwner.s.sol --private-key <DEST_USDC_BRIDGE_OWNER_PRIVATE_KEY> --broadcast
- Set the Circle's address in
USDCRolesHoldercontract:
USDC_ROLES_HOLDER_CIRCLE_ADDRESS=<ADDRESS_GIVEN_BY_CIRCLE> forge script ./script/usdc/for_circle_takeover/11_USDCRolesHolderSetCircle.s.sol --private-key <USDC_ROLES_HOLDER_OWNER_PRIVATE_KEY> --broadcast
Note
Bridge logic in this protocol assumes lossless 1:1 transfers. If USDT on your source chain has fee-on-transfer enabled, you may need to adjust the logic accordingly. See line 131 of the USDT contract on Ethereum Mainnet as reference for the fee-on-transfer logic.
-
Fill all
[*.usdt.*]fields except the ones ending with.deploymentinconfig/<mainnet or testnet>/config.toml. -
Run the deployment commands:
make usdt-and-bridge
-
Verify that
makeexited successfully, you should see a message with the ✅ emoji if all steps are completed without errors. -
Go to section 4 to test the bridge.
- Run the USDT deployment script after filling
[dest.usdt.init]:
make usdt-deploy
- Verify that
makeexited successfully, you should see a message with the ✅ emoji if all steps are completed without errors.
-
Fill the fields of
[dest.usdt.bridge.init]and[src.usdt.bridge.init]inconfig/<mainnet or testnet>/config.toml. -
Run the bridge deployment script:
make usdt-bridge-full
- Verify that
makeexited successfully, you should see a message with the ✅ emoji if all steps are completed without errors.
- Test the deployment by running the test script which sends 1 cent from source chain to destination chain, you need to have some USDT on source chain for this:
make usdt-bridge-mint-test
-
If the script is successful, search for the
sendtransaction (the other one is approval) in the output of the script on LayerZero Scan. Wait for the destination transaction hash, look it up on destination chain explorer, and confirm that destination chain USDT was minted to the address associated with the private key used above. -
Similarly, run the test script which sends 1 cent from destination chain to source chain, you need to have some bridged USDT on destination chain for this:
make usdt-bridge-burn-test
- If the script is successful, search for the
sendtransaction (the other one is approval) in the output of the script on LayerZero Scan. Wait for the destination transaction hash, look it up on source chain explorer, and confirm that USDT was burned from destination chain and 1 cent was sent to the address associated with the private key used above.
-
Fill all
[*.wbtc.*]fields except the ones ending with.deploymentinconfig/<mainnet or testnet>/config.toml. -
Run the bridge deployment script, this includes the WBTC token on the destination chain as the bridge acts as a token contract as well:
make wbtc-bridge-full
- Verify that
makeexited successfully, you should see a message with the ✅ emoji if all steps are completed without errors.
- Test the deployment by running the test script which sends 1 satoshi from source chain to destination chain, you need to have some WBTC on source chain for this:
make wbtc-bridge-mint-test
-
If the script is successful, search for the
sendtransaction (the other one is approval) in the output of the script on LayerZero Scan. Wait for the destination transaction hash, look it up on destination chain explorer, and confirm that destination chain WBTC was minted to the address associated with the private key used above. -
Similarly, run the test script which sends 1 satoshi from destination chain to source chain, you need to have some bridged WBTC on destination chain for this:
make wbtc-bridge-burn-test
- If the script is successful, search for the
sendtransaction (the other one is approval) in the output of the script on LayerZero Scan. Wait for the destination transaction hash, look it up on source chain explorer, and confirm that WBTC was burned from destination chain and 1 satoshi was sent to the address associated with the private key used above.