Deploy the adapter
The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. Here, Ethereum Sepolia is the hub chain and Arbitrum Sepolia is the source chain.
In this tutorial, we will:
- Deploy an adapter through the factory at
0x12A0f45738F3DDA931C4D3ECd1b267355e587921on Ethereum Sepolia, with your vault allowlisted and Arbitrum Sepolia accepted as a source chain. - Set an adapter fee of 0.001 CCIP-BnM on output that returns to Arbitrum Sepolia.
- Set the return format for the CCIP 2.0 lane to Arbitrum Sepolia, and fund the adapter with 0.01 Ethereum Sepolia ETH for return-leg CCIP fees.
- Check the configuration and preview a deposit and a redemption.
This tutorial continues from Deploy the vault and set up its share token, which deploys Vault CCIP-BnM (vCCIP-BnM), an ERC-4626 vault on Ethereum Sepolia whose asset is CCIP-BnM. You can use your own vault implementation instead.
Before you begin
You need:
- The
cross-chain-vault-adaptersrepository, cloned and built, with all the required variables being set in the.envfile. Set up your development environment lists the tools and the steps. - A Foundry keystore for your wallet, which deploys the adapter and receives its three roles in this tutorial. Use the same keystore in every tutorial of this solution.
- An ERC-4626 vault on Ethereum Sepolia. Deploy the vault and set up its share token deploys one.
1 Set your shell variables
Run every command in this tutorial from the root of the cross-chain-vault-adapters repository. Forge loads .env from there for every forge script run.
The cast commands in this tutorial read the following variables from your shell. If you opened a new terminal since the previous tutorial, change to the repository root and set them again:
export KEYSTORE_NAME=your_keystore_name
export ETHEREUM_SEPOLIA_RPC_URL=your_ethereum_sepolia_rpc_url
export ETHEREUM_SEPOLIA_CCIP_BNM=0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05
export VAULT=<your_vault_address>
Set MY_ADDRESS to your keystore's address. cast asks for the keystore password:
export MY_ADDRESS=$(cast wallet address --account $KEYSTORE_NAME)
echo $MY_ADDRESS
Confirm that VAULT is your vault and that its asset is CCIP-BnM:
cast call $VAULT "asset()(address)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
cast call $VAULT "symbol()(string)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
The output shows the CCIP-BnM address and the share symbol:
0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05
"vCCIP-BnM"
Deploy and configure the adapter
1 Configure the deployment
The pnpm ccip:deploy script reads its configuration from .env, calls deploy on the factory, and writes a deployment record.
View the deployment script on GitHub.
Print your wallet address and the vault address, which you paste into .env:
echo $MY_ADDRESS $VAULT
Open .env and set the following variables:
# CCIP Router on Ethereum Sepolia
ROUTER=0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
# Role holders. This tutorial gives all three roles to your wallet.
DEFAULT_ADMIN=<your_wallet_address>
FEE_SETTER=<your_wallet_address>
FEE_COLLECTOR=<your_wallet_address>
# Accept deposits and redemptions
DEPOSITS_ENABLED=true
REDEEMS_ENABLED=true
# The vault the adapter may call. TARGET_ENABLED defaults to true.
VAULT_TARGET=<your_vault_address>
# Accept messages from Arbitrum Sepolia (chain type 1 = EVM)
CHAIN_SELECTORS=3478487238524512106
CHAIN_TYPES=1
# Adapter fee of 0.001 CCIP-BnM on output returned to Arbitrum Sepolia.
# Row 1: deposits that return shares. Row 2: redemptions that return CCIP-BnM.
FEE_DESTINATION_CHAIN_SELECTORS=3478487238524512106,3478487238524512106
FEE_BRIDGED_TOKENS=<your_vault_address>,0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05
FEE_VALUES=1000000000000000,1000000000000000
# Deploy through the existing factory on Ethereum Sepolia
FACTORY=0x12A0f45738F3DDA931C4D3ECd1b267355e587921
What each setting does:
ROUTERis represents the CCIP Router address on Ethereum Sepolia, which you can verify from the CCIP Directory. The adapter stores it as the immutableROUTER, so a wrong router means deploying a new adapter.DEFAULT_ADMIN,FEE_SETTER, andFEE_COLLECTORreceive the adapter's three roles. The factory then renounces its own roles.- See Role handoff.
VAULT_TARGETconnects the adapter to your vault: the factory adds the vault to the adapter's allowlist,enabledTargets. Each user payload names the vault it targets, and the adapter rejects vaults that are not allowlisted.CHAIN_SELECTORSandCHAIN_TYPESaccept messages from Arbitrum Sepolia, whose selector number on the CCIP protocol is3478487238524512106. The adapter uses the same entry to encode return legs to that chain.FEE_DESTINATION_CHAIN_SELECTORS,FEE_BRIDGED_TOKENS, andFEE_VALUESare parallel lists, one fee row per position.- The
FACTORYvariable being set makes the script deploy through an existing factory instead of deploying a new one.
2 Simulate the deployment
We recommend simulating the deployment against Ethereum Sepolia, before actually executing on-chain. Without the --broadcast flag, Forge sends no transaction, and --sender runs the simulation from your wallet address:
pnpm ccip:deploy --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --sender $MY_ADDRESS
Before it simulates, the script performs a few checks:
- The
ROUTERandFACTORYcontracts have code, FACTORYshould be an instance of theCrossChainERC4626AdapterFactory 1.0.0,VAULT_TARGETanswersasset(),- That the chain type should be a valid number.
A successful simulation prints the configuration it would deploy (illustrative):
== Logs ==
Dry run: nothing was deployed. Re-run with --broadcast to deploy and write the deployment record.
CrossChainERC4626AdapterFactory: 0x12A0f45738F3DDA931C4D3ECd1b267355e587921
CrossChainERC4626Adapter: 0x...
Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
Default admin: 0x...
Fee setter / collector: 0x... 0x...
Vault target (enabled): 0x... true
Deposits / redeems enabled: true true
Chain configs / fee rows: 1 2
...
SIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.
Check that Chain configs / fee rows shows 1 2. The adapter address in the simulation matches the real one only if nobody else deploys through the factory before you broadcast, so read the real address from the deployment record after the broadcast.
3 Broadcast the deployment
Finally, we can deploy the adapter. Forge asks for your keystore password, sends one transaction that calls deploy on the factory, and verifies the new adapter on Etherscan:
pnpm ccip:deploy --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --account $KEYSTORE_NAME --broadcast --verify --delay 20 --retries 12
Your output should look something like this:
== Logs ==
CrossChainERC4626AdapterFactory: 0x12A0f45738F3DDA931C4D3ECd1b267355e587921
CrossChainERC4626Adapter: 0x...
...
Deployment record: deployments/ccip/11155111.json
Next steps:
1. Fund the adapter with native gas for return legs (ADAPTER=<adapter> FUND_NATIVE_WEI=...).
2. For CCIP v2 lanes, set return-lane formats and finality with pnpm ccip:configure.
3. Verify the result with pnpm ccip:check.
...
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
...
All (1) contracts were verified!
With --broadcast, the script writes a deployment record to deployments/ccip/11155111.json, where 11155111 is the Ethereum Sepolia chain ID.
You can print it using this command:
cat deployments/ccip/11155111.json
The record lists the factory, the adapter, the router, the role holders, the vault, and the processing switches (illustrative):
{
"adapter": "0x...",
"chainId": 11155111,
"defaultAdmin": "0x...",
"depositsEnabled": true,
"factory": "0x12A0f45738F3DDA931C4D3ECd1b267355e587921",
"feeCollector": "0x...",
"feeSetter": "0x...",
"redeemsEnabled": true,
"router": "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59",
"vaultTarget": "0x..."
}
Export the adapter address for the next steps. pnpm ccip:configure and pnpm ccip:check also read ADAPTER from your shell:
export ADAPTER=<your_adapter_address>
echo $ADAPTER
Open https://sepolia.etherscan.io/address/<ADAPTER> and confirm that the Contract tab shows verified source code for CrossChainERC4626Adapter.
4 Set the return format and fund the adapter
The adapter pays the CCIP fee for each return leg to Arbitrum Sepolia from its own Ethereum Sepolia ETH balance. With no balance, every request that returns output fails with InsufficientNativeBalance and becomes a failed message.
View the configuration script on GitHub.
The lane from Ethereum Sepolia to Arbitrum Sepolia is a CCIP 2.0 lane, and we need to tell the adapter that, using the RETURN_LANE_FORMATS param.
We can do that in one run, alongside funding the adapter with Ethereum Sepolia ETH.
RETURN_LANE_SELECTORS=3478487238524512106 \
RETURN_LANE_FORMATS=2 \
FUND_NATIVE_WEI=10000000000000000 \
pnpm ccip:configure --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --account $KEYSTORE_NAME --broadcast
- Before it sends anything, the script checks that
ADAPTERreportsCrossChainERC4626Adapter 1.0.0, so a mistyped address does not receive the Ethereum Sepolia ETH. - Setting the return format needs
DEFAULT_ADMIN_ROLE. Funding needs no role: anyone can send Ethereum Sepolia ETH to the adapter.
The script logs each setting it applied (illustrative):
== Logs ==
Return lane format set: 3478487238524512106 2
Funded adapter (wei): 10000000000000000
For what the return format and the other CCIP 2.0 settings control, see CCIP 2.0 lanes.
Verify the adapter
1 Run the deployment check
pnpm ccip:check reads the adapter and compares it with the values in .env:
pnpm ccip:check --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
The report shows the switches, the adapter's Ethereum Sepolia ETH balance, the role holders, the vault, and the settings for Arbitrum Sepolia (illustrative):
== Logs ==
Adapter: 0x...
typeAndVersion: CrossChainERC4626Adapter 1.0.0
Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
Deposits enabled: true
Redeems enabled: true
Native balance: 10000000000000000
DEFAULT_ADMIN holds role: 0x... true
FEE_SETTER holds role: 0x... true
FEE_COLLECTOR holds role: 0x... true
Vault target: 0x... true
Vault asset: 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05
Chain selector: 3478487238524512106
chain type: EVM
return format: GenericExtraArgsV3 basic (CCIP v2 lane)
inbound finality: 0x00000000
fee (share out): 1000000000000000
fee (asset out): 1000000000000000
Warnings: 0
Make sure that the report ends with Warnings: 0. The script prints each problem on a WARNING: line but still exits successfully, so read the count instead of relying on the exit code. An inbound finality of 0x00000000 is the default: the adapter accepts only messages that wait for finality.
2 Read the configuration with cast
Read the source chain, the vault allowlist, and the two fee rows directly from the adapter's public mappings:
cast call $ADAPTER "chains(uint64)(uint8)" 3478487238524512106 --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
cast call $ADAPTER "enabledTargets(address)(bool)" $VAULT --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
cast call $ADAPTER "assetFees(uint64,address)(uint256)" 3478487238524512106 $VAULT --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
cast call $ADAPTER "assetFees(uint64,address)(uint256)" 3478487238524512106 $ETHEREUM_SEPOLIA_CCIP_BNM --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
The output has one line per call:
1
true
1000000000000000 [1e15]
1000000000000000 [1e15]
| Call | Value | Meaning |
|---|---|---|
chains | 1 | Arbitrum Sepolia is an accepted source chain of type EVM (0 NONE, 2 SVM) |
enabledTargets | true | The adapter may call your vault |
assetFees with the vault address | 1000000000000000 | 0.001 CCIP-BnM fee on deposits whose shares return to Arbitrum Sepolia |
assetFees with the CCIP-BnM address | 1000000000000000 | 0.001 CCIP-BnM fee on redemptions whose CCIP-BnM returns to Arbitrum Sepolia |
3 Preview the adapter fee
preview simulates a request with the same routing and adapter fee as a real message from Arbitrum Sepolia. Preview a deposit of 0.1 CCIP-BnM (100000000000000000 wei) whose shares return to Arbitrum Sepolia:
cast call $ADAPTER "preview(address,address,uint256,bool,uint64)(uint256)" \
$ETHEREUM_SEPOLIA_CCIP_BNM $VAULT 100000000000000000 true 3478487238524512106 \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL
The adapter deducts the 0.001 CCIP-BnM fee before it deposits, so 0.099 CCIP-BnM enters the vault, and the 1:1 vault mints 0.099 vCCIP-BnM:
99000000000000000 [9.9e16]
Preview the same deposit with delivery on Ethereum Sepolia by passing false. The adapter charges no fee when output stays on Ethereum Sepolia:
cast call $ADAPTER "preview(address,address,uint256,bool,uint64)(uint256)" \
$ETHEREUM_SEPOLIA_CCIP_BNM $VAULT 100000000000000000 false 3478487238524512106 \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL
The output is the full 0.1 vCCIP-BnM:
100000000000000000 [1e17]
Preview a redemption of 0.1 vCCIP-BnM whose CCIP-BnM returns to Arbitrum Sepolia. For a redemption, the token is the vault address:
cast call $ADAPTER "preview(address,address,uint256,bool,uint64)(uint256)" \
$VAULT $VAULT 100000000000000000 true 3478487238524512106 \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL
The vault redeems 0.1 vCCIP-BnM for 0.1 CCIP-BnM, and the adapter deducts the 0.001 CCIP-BnM fee:
99000000000000000 [9.9e16]
A user sets minimumOut in the payload from this value minus a tolerance for exchange-rate movement. See Slippage protection.