Deploy the vault and set up its share token
In this tutorial, we will:
- Deploy
ExampleERC4626Vaulton Ethereum Sepolia and verify it on Etherscan. - Deposit 1 CCIP-BnM and receive 1 vCCIP-BnM.
- Transform
vCCIP-BnMinto a cross-chain token using the CCIP protocol under the Lock & Mint mechanism, by deploying:- A Lock & Release pool on Ethereum Sepolia, and,
- A Burn & Mint pool on Arbitrum Sepolia.
- Return to this page, transfer 0.1 vCCIP-BnM to Arbitrum Sepolia, and confirm the balances on both chains.
An ERC-4626 vault holds one asset and gives depositors shares in return. The vault contract is itself an ERC-20 token contract, and the shares are its tokens, so the vault's address is also the share token's address. In this tutorial, the asset is CCIP-BnM and the share token is Vault CCIP-BnM (vCCIP-BnM). Two adapter features need the share token to be a CCIP cross-chain token (CCT) on the Arbitrum Sepolia lane: returning shares to Arbitrum Sepolia, and redeeming shares from Arbitrum Sepolia.
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.
Before you begin
You need:
- Foundry,
- Node.js 22.10 or later,
- pnpm 10, and,
- A Foundry keystore for your wallet, which deploys and owns the vault and the share token contracts in this tutorial. Use the same keystore in every tutorial of this solution.
1 Set up your development environment
- Install ccip-cli and check its version:
npm install -g @chainlink/ccip-cli
ccip-cli --version
This tutorial was tested with ccip-cli 1.14.0.
- Check that Foundry is installed:
forge --version
- Enable pnpm through Corepack. The repository pins
pnpm@10.11.1:
corepack enable
- Clone the adapter repository, install its dependencies, and build it:
The adapter, its factory, the example vault, and their deploy scripts.
git clone https://github.com/smartcontractkit/cross-chain-vault-adapters.git
cd cross-chain-vault-adapters
pnpm install
forge build
- Create an encrypted Foundry keystore. Skip this step if
cast wallet listalready shows the keystore you want to use:
cast wallet import your_keystore_name --interactive
- Create a
.envfile from the example:
cp .env.example .env
- In
.env, fill in these variables:
ETHEREUM_SEPOLIA_RPC_URL=your_ethereum_sepolia_rpc_url
ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc_url
ETHERSCAN_API_KEY=your_etherscan_api_key
KEYSTORE_NAME=your_keystore_name
Leave the other variables as they are.
Run source .env to load the environment variables into your shell.
2 Set your shell variables
The cast and ccip-cli commands in these tutorials read the following variables from your shell. Use one terminal for this tutorial and the next one, and run these commands again if you open a new terminal:
You can fetch the keystore names available inside your Foundry keystore by using the
cast wallet listcommand.
export MY_ADDRESS=$(cast wallet address --account $KEYSTORE_NAME)
export ETHEREUM_SEPOLIA_ROUTER=0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
export ETHEREUM_SEPOLIA_CCIP_BNM=0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05
(cast wallet address --account $KEYSTORE_NAME)sets the public address of the keystore specified byKEYSTORE_NAMEinto your terminal. You will need it.
The RPC variable names match the Lock & Mint tutorial, so the same values work in both repositories. The router and CCIP-BnM addresses come from the Ethereum Sepolia page of the CCIP Directory.
3 Get CCIP-BnM on Ethereum Sepolia
You have two ways to get CCIP-BnM tokens on Ethereum Sepolia to follow through the tutorial:
- Mint CCIP-BnM on Ethereum Sepolia from CCIP test tokens.
- Use the following cast command to call
dripon the CCIP-BnM token:
cast send $ETHEREUM_SEPOLIA_CCIP_BNM "drip(address)" $MY_ADDRESS \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME
Check that your wallet holds at least 1 CCIP-BnM. CCIP-BnM has 18 decimals, so 1 CCIP-BnM is 1000000000000000000:
cast call $ETHEREUM_SEPOLIA_CCIP_BNM "balanceOf(address)(uint256)" $MY_ADDRESS \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL
Example Output:
1000000000000000000 [1e18]
Deploy and fund the vault
1 Deploy the vault
ExampleERC4626Vault combines OpenZeppelin's audited ERC4626 and Ownable contracts with a constructor and no other code.
This implementation is deliberately minimal to serve as a tool to follow through the tutorials. Please use your own, audited implementation in production.
Ownable gives the vault an owner() function. The Lock & Mint tutorial's Claim Admin Role step uses it to register you as the share token's CCIP administrator.
View the vault contract on GitHub.
DeployExampleERC4626Vault.s.solView the deployment script on GitHub.
- From the root of the
cross-chain-vault-adaptersrepository, deploy the vault with CCIP-BnM as its asset, and verify it on Etherscan:
VAULT_ASSET=$ETHEREUM_SEPOLIA_CCIP_BNM \
VAULT_NAME="Vault CCIP-BnM" \
VAULT_SYMBOL=vCCIP-BnM \
pnpm ccip:deploy-example-vault \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast \
--verify \
--delay 20 \
--retries 12 \
--gas-estimate-multiplier 700
Etherscan needs time to index a new contract. --delay 20 --retries 12 makes Forge retry verification up to 12 times, 20 seconds apart.
The script makes your wallet the vault owner. To make another address the owner, set VAULT_OWNER. Without --broadcast, the script only simulates the deployment.
Your output should look something like this:
== Return ==
vault: contract ExampleERC4626Vault 0xE4Ef963183C29401Ff6F67CC13B500dB1354cfB6
== Logs ==
ExampleERC4626Vault: 0xE4Ef963183C29401Ff6F67CC13B500dB1354cfB6
Asset: 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05
Name / symbol: Vault CCIP-BnM vCCIP-BnM
Decimals: 18
Owner: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
- Export the vault address from the
ExampleERC4626Vaultline:
export VAULT=<ExampleERC4626Vault_address>
- Check that your wallet owns the vault. The output must equal
$MY_ADDRESS:
cast call $VAULT "owner()(address)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
2 Deposit 1 CCIP-BnM
A deposit moves CCIP-BnM into the vault and mints vCCIP-BnM to the receiver. Amounts in these commands use 18 decimals: 1000000000000000000 is 1 token.
- Approve the vault to transfer 1 CCIP-BnM from your wallet:
cast send $ETHEREUM_SEPOLIA_CCIP_BNM "approve(address,uint256)" $VAULT 1000000000000000000 \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME
- Deposit 1 CCIP-BnM with your wallet as the receiver:
cast send $VAULT "deposit(uint256,address)" 1000000000000000000 $MY_ADDRESS \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME
Each cast send prints a receipt. Check that it shows status 1 (success).
- Check your vCCIP-BnM balance and the value of 1 vCCIP-BnM in CCIP-BnM:
cast call $VAULT "balanceOf(address)(uint256)" $MY_ADDRESS --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
cast call $VAULT "convertToAssets(uint256)(uint256)" 1000000000000000000 --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
At this point:
You hold 1 vCCIP-BnM, and 1 vCCIP-BnM converts to 1 CCIP-BnM.
Anyone can change this rate by transferring CCIP-BnM directly to the vault. The amounts in the next tutorials assume nobody has.
Set up the share token in CCIP
The Lock & Mint tutorial makes a token a cross-chain token with a Lock & Release pool on Ethereum Sepolia and a Burn & Mint pool on Arbitrum Sepolia. On Ethereum Sepolia, only the vault can mint vCCIP-BnM, so the pool locks and releases existing shares. On Arbitrum Sepolia, you deploy a new vCCIP-BnM token that its pool mints and burns.
1 Prepare the Lock & Mint repository
The Lock & Mint tutorial uses its own repository, docs-cct-foundry. Work in the same terminal, so VAULT and the other shell variables stay set.
- Move to the directory that contains
cross-chain-vault-adapters:
cd ..
-
Follow the Lock & Mint tutorial's Set up your development environment step.
-
From the
docs-cct-foundrydirectory, aftersource .env, point the Lock & Mint scripts at the vault as the Ethereum Sepolia token:
export ETHEREUM_SEPOLIA_TOKEN=$VAULT
You need to follow the rest of the tutorial as it is. You will essentially set up your Vault's token on Ethereum Sepolia as the canonical deployment and enable it to have cross-chain functionality via CCIP under the Lock & Mint mechanism.
2 Run the Lock & Mint tutorial for vCCIP-BnM
Follow the Lock & Mint tutorial from Deploy Tokens, with these changes:
| Lock & Mint step | What to do for vCCIP-BnM |
|---|---|
| Deploy Tokens | Skip the Ethereum Sepolia deployment, because the vault is the token there. Deploy only the Arbitrum Sepolia token with the command below, then export its address. Keep ETHEREUM_SEPOLIA_TOKEN=$VAULT. |
| Deploy Token Pools | Run every step: the ERC20LockBox, the Lock & Release pool, and the lockbox authorization on Ethereum Sepolia, and the Burn & Mint pool on Arbitrum Sepolia. |
| Claim Admin Role | Run it on both chains with CCIP_ADMIN_ADDRESS=$MY_ADDRESS. On Ethereum Sepolia, the script reads the admin from the vault's owner(). |
| Accept Admin Role | Run it on both chains. |
| Configure Token Pools | Run it on both chains with the tutorial's rate limits. |
| Configure Faster Than Finality | Skip it. These tutorials use default finality. |
| Activate the Token Pool | Run it on both chains. |
| Mint Tokens | Skip it. Only deposits mint vCCIP-BnM on Ethereum Sepolia, and you already hold 1 vCCIP-BnM. |
| Transfer Tokens Across Networks | Skip it. You transfer vCCIP-BnM in Test the share token. |
| Hand Off Token Administration | Skip it. |
Deploy the Arbitrum Sepolia token with the vault's name, symbol, and decimals, and no pre-mint. Because the decimals match, 0.1 vCCIP-BnM on Ethereum Sepolia arrives as 0.1 vCCIP-BnM on Arbitrum Sepolia:
TOKEN_NAME="Vault CCIP-BnM" \
TOKEN_SYMBOL=vCCIP-BnM \
TOKEN_DECIMALS=18 \
TOKEN_PRE_MINT=0 \
forge script \
script/deploy/DeployToken.s.sol \
--rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast \
--verify
Under Token Parameters, the output must show Vault CCIP-BnM, vCCIP-BnM, 18, and Pre-mint: 0. Export the token's address:
export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=<token_address_on_arbitrum_sepolia>
When Activate the Token Pool succeeds on both chains, return to this page.
3 Check the registration
- Once your token pool is activated on both chains, go back to the adapter repository:
cd ../cross-chain-vault-adapters
- Read the pool that the TokenAdminRegistry on each chain holds for the share token:
cast call 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 "getPool(address)(address)" $VAULT \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL
cast call 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f "getPool(address)(address)" $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN \
--rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL
- The first address must equal
$ETHEREUM_SEPOLIA_TOKEN_POOL, and, - The second must equal
$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL.
A zero address means Activate the Token Pool did not run on that chain.
Test the share token
1 Transfer vCCIP-BnM to Arbitrum Sepolia
Send 0.1 vCCIP-BnM from your wallet on Ethereum Sepolia to the same address on Arbitrum Sepolia. The Lock & Release pool locks the shares in the lockbox, and the Burn & Mint pool mints 0.1 vCCIP-BnM on Arbitrum Sepolia.
ccip-cli approves the router for the exact amount. Without --fee-token, it pays the CCIP fee in Ethereum Sepolia ETH:
ccip-cli send \
--source ethereum-testnet-sepolia \
--router $ETHEREUM_SEPOLIA_ROUTER \
--dest ethereum-testnet-sepolia-arbitrum-1 \
--transfer-tokens $VAULT=0.1 \
--receiver $MY_ADDRESS \
--wallet foundry:$KEYSTORE_NAME \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
--transfer-tokens takes the amount in whole tokens, so 0.1 is 0.1 vCCIP-BnM.
Your output should look something like this:
โ Enter password for Foundry keystore 'PRIVATE_KEY'
Estimated gasLimit for sender = 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 : 0
Fee: 257831635941687n = 0.000257831635941687 ETH
๐ Sending message to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x621f4cd397751b87bd8edb6bb90afb4e9bcfd274ccc228cb54e4d66d8bde787d , messageId => 0x81ef360a4235f6333e7725e38c171e018f14f73c19559aefb19ff70ab6ca3c9e
Lane:
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ (index) โ source โ dest โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ name โ 'ethereum-testnet-sepolia' โ 'ethereum-testnet-sepolia-arbitrum-1' โ
โ chainId โ 11155111 โ 421614 โ
โ chainSelector โ 16015286601757825753n โ 3478487238524512106n โ
โ onRamp/version โ '0x8dcf17f298c881A547D91ca4aA3C2AD7568C6777' โ '2.0.0' โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Request (source):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ (index) โ Values โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ messageId โ '0x81ef360a4235f6333e7725e38c171e018f14f73c19559aefb19ff70ab6ca3c9e' โ
โ origin โ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' โ
โ sender โ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' โ
โ receiver โ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' โ
Export the message ID from the output:
export MESSAGE_ID=<message_id>
2 Track the transfer
Open the CCIP Explorer link from the output, or follow the message from the terminal with ccip-cli show. --wait keeps the command running until the message executes on Arbitrum Sepolia:
ccip-cli show $MESSAGE_ID \
--wait \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
CCIP delivers the message after the source transaction reaches finality on Ethereum Sepolia, which typically takes about 20 minutes.
3 Confirm the balances
- Check your vCCIP-BnM balance on Ethereum Sepolia:
cast call $VAULT "balanceOf(address)(uint256)" $MY_ADDRESS --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
900000000000000000 [9e17]
If you set up the share token with the Lock & Mint tutorial in this terminal, LOCK_BOX holds the lockbox address from its Deploy ERC20LockBox step. Check that the lockbox holds the 0.1 vCCIP-BnM you sent:
cast call $VAULT "balanceOf(address)(uint256)" $LOCK_BOX --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
100000000000000000 [1e17]
- Check your vCCIP-BnM balance on Arbitrum Sepolia:
cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN "balanceOf(address)(uint256)" $MY_ADDRESS \
--rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL
100000000000000000 [1e17]
Your wallet holds 0.9 vCCIP-BnM on Ethereum Sepolia and 0.1 vCCIP-BnM on Arbitrum Sepolia. The lockbox holds the 0.1 vCCIP-BnM that backs the Arbitrum Sepolia supply. The vault still holds 1 CCIP-BnM against 1 vCCIP-BnM of total supply, so shares still convert 1:1.
Where to go next
- Deploy the adapter: deploy a Cross-Chain Vault Adapter for this vault on Ethereum Sepolia, with Arbitrum Sepolia as a source chain. Stay in this terminal and in the
cross-chain-vault-adaptersdirectory. - How it works: how the adapter uses the vault and its share token for deposits and redemptions.