Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
950e2f6
feat: add configuration for Ethereum and Arbitrum chains in config.json
gfournierPro Jul 29, 2025
6d1eda3
feat: enhance cross-chain transfer scripts and update configuration f…
gfournierPro Jul 29, 2025
6774976
feat: refactor verification targets for Ethereum and Arbitrum networks
gfournierPro Jul 29, 2025
7ea4c73
feat: implement contract verification for multiple networks in deploy…
gfournierPro Jul 29, 2025
3756bdd
feat: update recipient address retrieval in cross-chain transfer scripts
gfournierPro Jul 29, 2025
111d3ef
feat: simplify deployment and upgrade targets by removing OPTIONS par…
gfournierPro Jul 30, 2025
4142ea8
fix: forge fmt
gfournierPro Jul 30, 2025
2492ff8
fix: update initial addresses in config.json
gfournierPro Jul 30, 2025
376ee66
Merge branch 'main' into feature/prepare-mainnet
gfournierPro Jul 30, 2025
8e0487e
fix: reset Createx salts to zero for Ethereum and Arbitrum configurat…
gfournierPro Jul 30, 2025
468ec90
fix: clear default values for Etherscan API key and recipient address…
gfournierPro Jul 30, 2025
cbf938a
fix: update RPC URLs in .env.template to use Tenderly gateway
gfournierPro Jul 30, 2025
1463754
fix: simplify RPC URL handling in deployment and verification scripts
gfournierPro Jul 31, 2025
fbaa270
Merge branch 'main' into feature/prepare-mainnet
gfournierPro Jul 31, 2025
02fee6b
feat: add TODO for merging cross-chain token transfer scripts
gfournierPro Jul 31, 2025
583fba5
fix: forge fmt
gfournierPro Jul 31, 2025
a276c24
feat: parametrize config.json for mainnet and testnet separation
gfournierPro Jul 31, 2025
6359848
fix: allow contract verification step to continue on error
gfournierPro Jul 31, 2025
61694ad
fix: update empty addresses in config.json for ethereum and arbitrum
gfournierPro Jul 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
CI=false # Set to true if you want to run the make file commands in CI mode (will use --private-key instead of --account option)
# ===========================================
# CI/CD CONFIGURATION
# ===========================================
# Set to true in CI environments
CI=false

# ===========================================
# DEPLOYMENT CONFIGURATION
# ===========================================
# Account name in your Foundry wallet for deployment
ACCOUNT=default

# ===========================================
# RPC_URL CONFIGURATION
# ===========================================
ETHEREUM_RPC_URL="https://gateway.tenderly.co/public/mainnet"
ARBITRUM_RPC_URL="https://arbitrum.gateway.tenderly.co"
SEPOLIA_RPC_URL="https://gateway.tenderly.co/public/sepolia"
ARBITRUM_SEPOLIA_RPC_URL="https://arbitrum-sepolia.gateway.tenderly.co"

# ===========================================
# ANVIL LOCAL TESTING CONFIGURATION
# ===========================================
ANVIL_SEPOLIA_RPC_URL=http://localhost:8545
ANVIL_ARBITRUM_SEPOLIA_RPC_URL=http://localhost:8546

# ===========================================
# VERIFICATION CONFIGURATION
# ===========================================
# Etherscan v2 uses a single API key for all networks.
ETHERSCAN_API_URL=https://api.etherscan.io/v2/api
# Etherscan API key for contract verification
ETHERSCAN_API_KEY=

# Local dev config.
ANVIL_SEPOLIA_RPC_URL=http://localhost:8545
ANVIL_ARBITRUM_SEPOLIA_RPC_URL=http://localhost:8546

# Account to be used for script execution.
# Account name in Foundry keystore
ACCOUNT=<your-account-name>
RECIPIENT_ADDRESS=<recipient-address> # for cross-chain transfers scripts
# ===========================================
# TRANSFER CONFIGURATION
# ===========================================
# Recipient address for cross-chain transfers
RECIPIENT_ADDRESS=
6 changes: 5 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,9 @@ jobs:

- name: Verify contracts
if: inputs.network != 'anvil'
continue-on-error: true
env:
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
RPC_URL: ${{ secrets.RPC_URL }}
run: |
echo "TODO: Implement contract verification for ${{ inputs.network }}."
make verify-all-${{ inputs.network }}
56 changes: 35 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,31 +52,28 @@ deploy-on-anvil:
$(MAKE) deploy-all \
SOURCE_CHAIN=sepolia SOURCE_RPC=$(ANVIL_SEPOLIA_RPC_URL) \
TARGET_CHAIN=arbitrum_sepolia TARGET_RPC=$(ANVIL_ARBITRUM_SEPOLIA_RPC_URL) \
OPTIONS=

deploy-on-mainnets:
$(MAKE) deploy-all \
SOURCE_CHAIN=ethereum SOURCE_RPC=$(ETHEREUM_RPC_URL) \
TARGET_CHAIN=arbitrum TARGET_RPC=$(ARBITRUM_RPC_URL) \
OPTIONS=--verify

deploy-on-testnets:
$(MAKE) deploy-all \
SOURCE_CHAIN=sepolia SOURCE_RPC=$(SEPOLIA_RPC_URL) \
TARGET_CHAIN=arbitrum_sepolia TARGET_RPC=$(ARBITRUM_SEPOLIA_RPC_URL) \
OPTIONS="--verify --verifier etherscan --verifier-api-key $(ETHERSCAN_API_KEY) --verifier-url $(ETHERSCAN_API_URL)"

deploy-liquidity-unifier-and-bridge:
$(MAKE) deploy-contract CONTRACT=RLCLiquidityUnifier CHAIN=$(CHAIN) RPC_URL=$(RPC_URL) OPTIONS="$(OPTIONS)"
$(MAKE) deploy-contract CONTRACT=bridges/layerZero/IexecLayerZeroBridge CHAIN=$(CHAIN) RPC_URL=$(RPC_URL) OPTIONS="$(OPTIONS)"
$(MAKE) deploy-contract CONTRACT=RLCLiquidityUnifier CHAIN=$(CHAIN) RPC_URL=$(RPC_URL)
$(MAKE) deploy-contract CONTRACT=bridges/layerZero/IexecLayerZeroBridge CHAIN=$(CHAIN) RPC_URL=$(RPC_URL)

deploy-crosschain-token-and-bridge:
$(MAKE) deploy-contract CONTRACT=RLCCrosschainToken CHAIN=$(CHAIN) RPC_URL=$(RPC_URL) OPTIONS="$(OPTIONS)"
$(MAKE) deploy-contract CONTRACT=bridges/layerZero/IexecLayerZeroBridge CHAIN=$(CHAIN) RPC_URL=$(RPC_URL) OPTIONS="$(OPTIONS)"
$(MAKE) deploy-contract CONTRACT=RLCCrosschainToken CHAIN=$(CHAIN) RPC_URL=$(RPC_URL)
$(MAKE) deploy-contract CONTRACT=bridges/layerZero/IexecLayerZeroBridge CHAIN=$(CHAIN) RPC_URL=$(RPC_URL)

deploy-all: # SOURCE_CHAIN, SOURCE_RPC, TARGET_CHAIN, TARGET_RPC, OPTIONS
$(MAKE) deploy-liquidity-unifier-and-bridge CHAIN=$(SOURCE_CHAIN) RPC_URL=$(SOURCE_RPC) OPTIONS=$(OPTIONS)
$(MAKE) deploy-crosschain-token-and-bridge CHAIN=$(TARGET_CHAIN) RPC_URL=$(TARGET_RPC) OPTIONS=$(OPTIONS)
deploy-all: # SOURCE_CHAIN, SOURCE_RPC, TARGET_CHAIN, TARGET_RPC
$(MAKE) deploy-liquidity-unifier-and-bridge CHAIN=$(SOURCE_CHAIN) RPC_URL=$(SOURCE_RPC)
$(MAKE) deploy-crosschain-token-and-bridge CHAIN=$(TARGET_CHAIN) RPC_URL=$(TARGET_RPC)
@echo "Contracts deployment completed."
@echo "⚠️ Run 'make configure-all' to configure bridges."
@echo "⚠️ Please configure the bridges. Do not forget to authorize the RLCLiquidityUnifier and RLCCrosschainToken contracts on the bridges."
Expand All @@ -99,43 +96,39 @@ upgrade-on-mainnets:
$(MAKE) upgrade-all \
SOURCE_CHAIN=ethereum SOURCE_RPC=$(ETHEREUM_RPC_URL) \
TARGET_CHAIN=arbitrum TARGET_RPC=$(ARBITRUM_RPC_URL) \
OPTIONS=--verify

# TODO : RLCMultichain and RLCLiquidityUnifier upgrades
upgrade-on-testnets:
$(MAKE) upgrade-all \
SOURCE_CHAIN=sepolia SOURCE_RPC=$(SEPOLIA_RPC_URL) \
TARGET_CHAIN=arbitrum_sepolia TARGET_RPC=$(ARBITRUM_SEPOLIA_RPC_URL) \
OPTIONS=--verify

upgrade-all: # SOURCE_CHAIN, SOURCE_RPC, TARGET_CHAIN, TARGET_RPC, OPTIONS
$(MAKE) upgrade-contract CONTRACT=bridges/layerZero/IexecLayerZeroBridge CHAIN=$(SOURCE_CHAIN) RPC_URL=$(SOURCE_RPC) OPTIONS=$(OPTIONS)
$(MAKE) upgrade-contract CONTRACT=bridges/layerZero/IexecLayerZeroBridge CHAIN=$(TARGET_CHAIN) RPC_URL=$(TARGET_RPC) OPTIONS=$(OPTIONS)
upgrade-all: # SOURCE_CHAIN, SOURCE_RPC, TARGET_CHAIN, TARGET_RPC
$(MAKE) upgrade-contract CONTRACT=bridges/layerZero/IexecLayerZeroBridge CHAIN=$(SOURCE_CHAIN) RPC_URL=$(SOURCE_RPC)
$(MAKE) upgrade-contract CONTRACT=bridges/layerZero/IexecLayerZeroBridge CHAIN=$(TARGET_CHAIN) RPC_URL=$(TARGET_RPC)

#
# Generic deployment targets
#

deploy-contract: # CONTRACT, CHAIN, RPC_URL, OPTIONS
@echo "Deploying $(CONTRACT) on $(CHAIN) with options: $(OPTIONS)"
deploy-contract: # CONTRACT, CHAIN, RPC_URL
@echo "Deploying $(CONTRACT) on $(CHAIN)"
CHAIN=$(CHAIN) forge script script/$(CONTRACT).s.sol:Deploy \
--rpc-url $(RPC_URL) \
$$(if [ "$(CI)" = "true" ]; then echo "--private-key $(DEPLOYER_PRIVATE_KEY)"; else echo "--account $(ACCOUNT)"; fi) \
$(OPTIONS) \
--broadcast \
-vvv

#
# Generic upgrade targets
#

upgrade-contract: # CONTRACT, CHAIN, RPC_URL, OPTIONS
@echo "Upgrading $(CONTRACT) on $(CHAIN) with options: $(OPTIONS)"
upgrade-contract: # CONTRACT, CHAIN, RPC_URL
@echo "Upgrading $(CONTRACT) on $(CHAIN)"
CHAIN=$(CHAIN) forge script script/$(CONTRACT).s.sol:Upgrade \
--rpc-url $(RPC_URL) \
--account $(ACCOUNT) \
--broadcast \
$(OPTIONS) \
-vvv

#
Expand All @@ -162,6 +155,8 @@ upgrade-layerzero-bridge: # CHAIN, RPC_URL
# Bridge operations.
#

# Testnet bridge operations

send-tokens-to-arbitrum-sepolia:
@echo "Sending tokens cross-chain... from SEPOLIA to Arbitrum SEPOLIA"
SOURCE_CHAIN=sepolia TARGET_CHAIN=arbitrum_sepolia \
Expand All @@ -179,3 +174,22 @@ send-tokens-to-sepolia:
--account $(ACCOUNT) \
--broadcast \
-vvv

# Mainnet bridge operations
send-tokens-to-arbitrum-mainnet:
@echo "Sending tokens cross-chain... from ETHEREUM to Arbitrum MAINNET"
SOURCE_CHAIN=ethereum TARGET_CHAIN=arbitrum \
forge script script/SendFromEthereumToArbitrum.s.sol:SendFromEthereumToArbitrum \
--rpc-url $(ETHEREUM_RPC_URL) \
--account $(ACCOUNT) \
--broadcast \
-vvv

send-tokens-to-ethereum-mainnet:
@echo "Sending tokens cross-chain... from Arbitrum MAINNET to ETHEREUM"
SOURCE_CHAIN=arbitrum TARGET_CHAIN=ethereum \
forge script script/SendFromArbitrumToEthereum.s.sol:SendFromArbitrumToEthereum \
--rpc-url $(ARBITRUM_RPC_URL) \
--account $(ACCOUNT) \
--broadcast \
-vvv
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,17 @@ The core contracts of the multichain bridge system:

## Usage

### Bridge RLC
### Network Support

The bridge currently supports:

#### **Testnets**
- **Ethereum Sepolia** ↔ **Arbitrum Sepolia**

#### **Mainnets**
- **Ethereum Mainnet** ↔ **Arbitrum Mainnet**

### Bridge RLC on Testnets

A. To send RLC tokens from Ethereum Sepolia to Arbitrum Sepolia:

Expand Down Expand Up @@ -144,6 +154,20 @@ This will:
2. Send a cross-chain message via LayerZero to Ethereum
3. Release the original RLC tokens from the RLCLiquidityUnifier on Ethereum

### Bridge RLC on Mainnets

A. To send RLC tokens from Ethereum Mainnet to Arbitrum Mainnet:

```bash
make send-tokens-to-arbitrum-mainnet
```

B. To send RLC tokens from Arbitrum Mainnet back to Ethereum Mainnet:

```bash
make send-tokens-to-ethereum-mainnet
```

## 📊 Code Coverage Analysis

### Generating Coverage Reports
Expand Down Expand Up @@ -314,3 +338,9 @@ The scripts automatically calculate these fees and include them in the transacti
- Use an enterprise RPC URL for `secrets.SEPOLIA_RPC_URL` in Github environment `ci`.
- Add git pre-commit hook to format code locally.
- Testing Documentation
- Parametrize config.json to not overide btw mainnet and testnets
```
"initialAdmin": "0x111165a109feca14e4ad4d805f6460c7d206ead1",
"initialUpgrader": "0x111121e2ec2557f484f65d5b1ad2b6b07b8acd23",
"initialPauser": "0x11113fe3513787f5a4f5f19690700e2736b3056e",
```
25 changes: 22 additions & 3 deletions config/config.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"createxFactory": "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed",
"initialAdmin": "0x9990cfb1Feb7f47297F54bef4d4EbeDf6c5463a3",
"initialUpgrader": "0x9990cfb1Feb7f47297F54bef4d4EbeDf6c5463a3",
"initialPauser": "0x9990cfb1Feb7f47297F54bef4d4EbeDf6c5463a3",
"initialAdmin": "0x111165a109feca14e4ad4d805f6460c7d206ead1",
"initialUpgrader": "0x111121e2ec2557f484f65d5b1ad2b6b07b8acd23",
"initialPauser": "0x11113fe3513787f5a4f5f19690700e2736b3056e",
Comment on lines +3 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a TODO for that in the readme ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean
todo for ?

"chains": {
"sepolia": {
"approvalRequired": true,
Expand All @@ -22,6 +22,25 @@
"iexecLayerZeroBridgeCreatexSalt": "0x4f2784ad07b2be2a5c5e466c91d758133f4aa33bd4cf09ddba1a1e1035e57875",
"lzEndpointAddress": "0x6EDCE65403992e310A62460808c4b910D972f10f",
"lzChainId": 40231
},
"ethereum": {
"approvalRequired": true,
"rlcAddress": "0x607F4C5BB672230e8672085532f7e901544a7375",
"rlcLiquidityUnifierAddress": "0x0000000000000000000000000000000000000000",
"rlcLiquidityUnifierCreatexSalt": "0x0000000000000000000000000000000000000000000000000000000000000000",
"iexecLayerZeroBridgeAddress": "0x0000000000000000000000000000000000000000",
"iexecLayerZeroBridgeCreatexSalt": "0x0000000000000000000000000000000000000000000000000000000000000000",
"lzEndpointAddress": "0x1a44076050125825900e736c501f859c50fE728c",
"lzChainId": 30101
},
"arbitrum": {
"approvalRequired": false,
"rlcCrosschainTokenAddress": "0x0000000000000000000000000000000000000000",
"rlcCrosschainTokenCreatexSalt": "0x0000000000000000000000000000000000000000000000000000000000000000",
"iexecLayerZeroBridgeAddress": "0x0000000000000000000000000000000000000000",
"iexecLayerZeroBridgeCreatexSalt": "0x0000000000000000000000000000000000000000000000000000000000000000",
"lzEndpointAddress": "0x1a44076050125825900e736c501f859c50fE728c",
"lzChainId": 30110
}
}
}
74 changes: 48 additions & 26 deletions script/SendFromArbitrumToEthereum.s.sol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 Do we really need two scripts? Can’t we have a single contract script that handles the transfer in both directions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could yes
will make a todo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
pragma solidity ^0.8.22;

import {Script, console} from "forge-std/Script.sol";
Comment on lines 3 to 5
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No important change just cleaning

import {ConfigLib} from "./lib/ConfigLib.sol";
import {IexecLayerZeroBridge} from "../src/bridges/layerZero/IexecLayerZeroBridge.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SendParam} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import {MessagingFee} from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import {IexecLayerZeroBridge} from "../src/bridges/layerZero/IexecLayerZeroBridge.sol";
import {ConfigLib} from "./lib/ConfigLib.sol";

/**
* Script to send tokens from Arbitrum Mainnet to Ethereum Mainnet.
* This script demonstrates cross-chain token transfers using LayerZero bridge.
*/
// TODO: fusion SendTokensFromArbitrumToEthereum and SendTokensFromEthereumToArbitrum into a single script with dynamic chain handling
contract SendTokensFromArbitrumToEthereum is Script {
uint256 private constant TRANSFER_AMOUNT = 10 * 10 ** 9; // 10 RLC tokens with 9 decimals

/**
* @dev Converts an address to bytes32.
* @param _addr The address to convert.
Expand All @@ -18,45 +26,59 @@ contract SendTokensFromArbitrumToEthereum is Script {
return bytes32(uint256(uint160(_addr)));
}

/**
* Main function to execute the cross-chain transfer.
* Reads configuration and sends tokens from Arbitrum to Ethereum.
*/
function run() external {
string memory sourceChain = vm.envString("SOURCE_CHAIN");
string memory targetChain = vm.envString("TARGET_CHAIN");

ConfigLib.CommonConfigParams memory sourceParams = ConfigLib.readCommonConfig(sourceChain);
ConfigLib.CommonConfigParams memory targetParams = ConfigLib.readCommonConfig(targetChain);

// Contract addresses
address iexecLayerZeroBridgeAddress = sourceParams.iexecLayerZeroBridgeAddress;

// Transfer parameters
uint16 destinationChainId = uint16(targetParams.lzChainId); // LayerZero chain ID for Ethereum Sepolia
address recipientAddress = vm.envAddress("RECIPIENT_ADDRESS");
console.log("Recipient: %s", recipientAddress);
IexecLayerZeroBridge sourceBridge = IexecLayerZeroBridge(sourceParams.iexecLayerZeroBridgeAddress);
IERC20 rlcToken = IERC20(sourceParams.rlcCrosschainTokenAddress);

uint256 amount = 5 * 10 ** 9; // RLC tokens (adjust the amount as needed)
address sender = vm.envAddress("RECIPIENT_ADDRESS");
address recipient = vm.envAddress("RECIPIENT_ADDRESS");

// Send tokens cross-chain
IexecLayerZeroBridge iexecLayerZeroBridge = IexecLayerZeroBridge(iexecLayerZeroBridgeAddress);
console.log("Sending %s RLC to Ethereum Sepolia", amount / 10 ** 9);
// Check sender's balance
uint256 senderBalance = rlcToken.balanceOf(sender);
require(senderBalance >= TRANSFER_AMOUNT, "Insufficient RLC balance");

// Prepare send parameters
SendParam memory sendParam = SendParam(
destinationChainId, // Destination endpoint ID.
addressToBytes32(recipientAddress), // Recipient address.
amount, // amount (in local decimals, e.g., 5 RLC = 5 * 10 ** 9)
amount * 99 / 100, // minAmount (allowing 1% slippage)
"", // Extra options, not used in this case, already setup using `setEnforcedOptions`
"", // Composed message, not used in this case
"" // OFT command to be executed, unused in default OFT implementations.
uint16(targetParams.lzChainId), // Destination endpoint ID
addressToBytes32(recipient), // Recipient address
TRANSFER_AMOUNT, // Amount to send in local decimals
TRANSFER_AMOUNT * 99 / 100, // Minimum amount to send (allowing 1% slippage)
"", // Extra options, already set via setEnforcedOptions
"", // Composed message for send() operation (unused)
"" // OFT command to be executed (unused in default OFT)
);

// Get the fee for the transfer
MessagingFee memory fee = iexecLayerZeroBridge.quoteSend(sendParam, false);
console.log("Fee amount: ", fee.nativeFee);
// Get quote for the transfer
MessagingFee memory fee = sourceBridge.quoteSend(sendParam, false);

console.log("=== Cross-Chain Transfer Details ===");
console.log("From: Arbitrum Mainnet");
console.log("To: Ethereum Mainnet");
console.log("Amount: %d RLC", TRANSFER_AMOUNT / 10 ** 9);
console.log("Fee: %d wei", fee.nativeFee);
console.log("Sender: %s", sender);
console.log("Recipient: %s", recipient);
console.log("Sender Balance: %d RLC", senderBalance / 10 ** 9);

vm.startBroadcast();
// Execute the cross-chain transfer
iexecLayerZeroBridge.send{value: fee.nativeFee}(sendParam, fee, msg.sender);

console.log("Cross-chain transfer from Arbitrum to Ethereum initiated!");
// Note: For crosschain tokens, no approval is needed as the bridge can burn directly
console.log("Initiating cross-chain transfer...");
sourceBridge.send{value: fee.nativeFee}(sendParam, fee, payable(sender));

vm.stopBroadcast();

console.log("Transfer initiated successfully!");
console.log("Monitor the destination chain for token receipt.");
}
}
Loading