Cross-Chain Vault Adapter
Uses CCIP View on GitHub

Factory contract

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.

CrossChainERC4626AdapterFactory deploys a CrossChainERC4626Adapter on the hub chain, applies its initial configuration, and hands its roles to your accounts in one transaction.

This contract provides:

  • deployment of an adapter bound to the CCIP Router you pass in
  • the initial chain types, vault allowlist entry, deposit and redeem switches, and adapter fees
  • a role handoff that leaves the factory with no roles on the adapter

Usage boundary

  • Anyone can call deploy. The factory has no owner, allowlist, or fee.
  • After deploy returns, the factory holds no role on the adapter and cannot change it.
  • The factory keeps no registry of adapters. The AdapterDeployed event is the only on-chain record that links an adapter to this factory.
  • The factory sets the initial configuration only. Send later changes to the adapter directly, from the account that holds the required role. See the configuration functions on the adapter.

Contract

src/ccip/CrossChainERC4626AdapterFactory.sol

  • Version: CrossChainERC4626AdapterFactory 1.0.0, returned by typeAndVersion.
  • The factory and the adapter are audited.
  • Both are compiled with solc 0.8.24 for the cancun EVM version, and their bytecode uses the PUSH0 opcode. Deploy them only on chains that support the Shanghai upgrade.

Deployed addresses

NetworkAddress
Ethereum mainnet0x1b79038DCCbeE0E406eA0c1bA758A2cF696b38D4
Ethereum Sepolia0x12A0f45738F3DDA931C4D3ECd1b267355e587921

You can deploy your own factory with the repository's pnpm ccip:deploy-factory script.

Inheritance

Functions

deploy

Deploys and configures a CrossChainERC4626Adapter in one transaction.

function deploy(DeploymentConfig calldata config) external returns (address adapterAddress)

The factory holds all three roles while it applies config, then grants each role to its configured account and renounces its own. Any revert undoes the whole transaction, so no adapter is created. The numbered steps after Emits list the exact order.

deploy leaves the CCIP 2.0 settings at their defaults and does not fund the adapter. See Security notes.

Access: anyone.

Parameters:

ParameterTypeDescription
configDeploymentConfig calldataRouter, role accounts, vault, processing switches, and batches of chain and fee rows.

Returns:

NameTypeDescription
adapterAddressaddressAddress of the deployed adapter.

Reverts with:

  • InvalidAdmin, InvalidFeeSetter, or InvalidFeeCollector when the matching role account is the zero address.
  • InvalidRouter(address(0)) from the adapter constructor when config.router is the zero address.
  • InvalidTarget(address(0)) from the adapter's setAssetFee when a FeeConfig row has a zero bridgedToken.

Emits:

  • AdapterDeployed from the factory.
  • The adapter's own events from the configuration calls: ChainTypeSet per chain row, TargetEnabled when vaultTarget is set, ProcessingEnabledSet, and AssetFeeSet per fee row. See the adapter's events.
  • OpenZeppelin RoleGranted and RoleRevoked events from the adapter for the role handoff.

deploy runs these steps in order:

  1. Reverts if defaultAdmin, feeSetter, or feeCollector is the zero address. These checks run before the adapter constructor, so a call with a zero router and a zero admin reverts with InvalidAdmin.
  2. Deploys the adapter with CREATE, passing config.router and the factory's own address as admin, fee setter, and fee collector. The factory now holds all three roles.
  3. Calls setChainType once for each chainConfigs row.
  4. Calls setTargetEnabled(vaultTarget, targetEnabled) only when vaultTarget is not the zero address. Otherwise it skips the call and ignores targetEnabled.
  5. Calls setProcessingEnabled(depositsEnabled, redeemsEnabled). This call always runs.
  6. Calls setAssetFee once for each feeConfigs row.
  7. Hands off the roles, as described in Role handoff.
  8. Emits AdapterDeployed and returns the adapter address.

Rows apply in array order. A later row for the same chain selector, or the same selector and token pair, overwrites an earlier one.


typeAndVersion

Returns the factory's type and version, declared as a public constant:

string public constant override typeAndVersion = "CrossChainERC4626AdapterFactory 1.0.0";

Access: anyone.

Returns: string, the contract type and version. The repository's pnpm ccip:deploy script checks this value before it deploys through an existing factory.

Types

The three structs are declared in the factory:

struct ChainConfig {
  uint64 chainSelector;
  CrossChainERC4626Adapter.ChainType chainType;
}

struct FeeConfig {
  uint64 destinationChainSelector;
  address bridgedToken;
  uint256 fee;
}

struct DeploymentConfig {
  address router;
  address defaultAdmin;
  address feeSetter;
  address feeCollector;
  address vaultTarget;
  bool targetEnabled;
  bool depositsEnabled;
  bool redeemsEnabled;
  ChainConfig[] chainConfigs;
  FeeConfig[] feeConfigs;
}

DeploymentConfig

Field
Type
Description
routeraddressCCIP Router on the hub chain. It becomes the adapter's immutable ROUTER, the only caller allowed to deliver messages. Must not be zero. Get the address from the CCIP Directory for testnet or mainnet.
defaultAdminaddressReceives DEFAULT_ADMIN_ROLE. Must not be zero.
feeSetteraddressReceives FEE_SETTER_ROLE. Must not be zero.
feeCollectoraddressReceives FEE_COLLECTOR_ROLE. Must not be zero.
vaultTargetaddressERC-4626 vault to allowlist. Pass the zero address to skip.
targetEnabledboolValue passed to setTargetEnabled for vaultTarget. Ignored when vaultTarget is zero.
depositsEnabledboolWhether the adapter processes deposits.
redeemsEnabledboolWhether the adapter processes redemptions.
chainConfigsChainConfig[]Source chains to configure. Can be empty.
feeConfigsFeeConfig[]Adapter fee rows. Can be empty.

ChainConfig

FieldTypeDescription
chainSelectoruint64CCIP chain selector of a source chain.
chainTypeCrossChainERC4626Adapter.ChainTypeNONE (0) disables the chain, EVM (1) is an EVM chain, SVM (2) is the Solana Virtual Machine chain family. Messages from a chain whose type is NONE become failed messages in the adapter.

FeeConfig

Each row becomes one setAssetFee call. The code calls the adapter fee the asset fee.

Field
Type
Description
destinationChainSelectoruint64Chain the output returns to, which is the source chain of the inbound message.
bridgedTokenaddressToken bridged back, which is the share token (the vault address) for deposit returns and the asset for redeem returns. Must not be zero.
feeuint256Flat adapter fee in the asset's smallest units. The adapter charges it only when the user asks for return to the source chain.

The fee is staged even when the chain in that row has no chain type yet.

Example configuration

The following JSON illustrates the fields of a DeploymentConfig for an adapter on Ethereum Sepolia that accepts messages from Arbitrum Sepolia, with deposits and redemptions enabled and an adapter fee on both return legs. No repository script reads this JSON. Instead, pnpm ccip:deploy builds the struct from environment variables. Chain selectors and fees are strings because uint64 and uint256 values can exceed the JavaScript safe integer range.

{
  "router": "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59",
  "defaultAdmin": "<your admin multisig>",
  "feeSetter": "<your fee setter>",
  "feeCollector": "<your fee collector>",
  "vaultTarget": "<your vault address>",
  "targetEnabled": true,
  "depositsEnabled": true,
  "redeemsEnabled": true,
  "chainConfigs": [{ "chainSelector": "3478487238524512106", "chainType": 1 }],
  "feeConfigs": [
    {
      "destinationChainSelector": "3478487238524512106",
      "bridgedToken": "<your vault address>",
      "fee": "1000000000000000"
    },
    {
      "destinationChainSelector": "3478487238524512106",
      "bridgedToken": "<your vault asset address>",
      "fee": "1000000000000000"
    }
  ]
}
  • 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 is the CCIP Router on Ethereum Sepolia, and 3478487238524512106 is the Arbitrum Sepolia chain selector.
  • Replace <your vault address>, <your vault asset address>, and the three role placeholders with your own addresses.
  • A fee of 1000000000000000 is 0.001 of an 18-decimal asset. It matches the 0.001 CCIP-BnM adapter fee in Deploy the adapter.
  • The first fee row applies when shares return to Arbitrum Sepolia. That requires the share token to be a CCIP cross-chain token (CCT) on both chains.

Events

The factory declares one event:

event AdapterDeployed(
  address indexed adapter,
  address indexed router,
  address indexed defaultAdmin,
  address feeSetter,
  address feeCollector,
  address vaultTarget,
  bool targetEnabled,
  bool depositsEnabled,
  bool redeemsEnabled
);
Field
Type
IndexedDescription
adapteraddressYesAddress of the new adapter.
routeraddressYesconfig.router.
defaultAdminaddressYesAccount that received DEFAULT_ADMIN_ROLE.
feeSetteraddressNoAccount that received FEE_SETTER_ROLE.
feeCollectoraddressNoAccount that received FEE_COLLECTOR_ROLE.
vaultTargetaddressNoconfig.vaultTarget, or the zero address if none was set.
targetEnabledboolNoconfig.targetEnabled, emitted even when vaultTarget is zero.
depositsEnabledboolNoconfig.depositsEnabled.
redeemsEnabledboolNoconfig.redeemsEnabled.

The chain and fee rows are not in this event. They appear as the adapter's ChainTypeSet and AssetFeeSet events in the same transaction.

Errors

The factory declares no errors. It reverts with errors declared in CrossChainERC4626Adapter, listed on the adapter's errors:

ErrorRaised byWhen
InvalidAdmin()FactorydefaultAdmin is the zero address.
InvalidFeeSetter()FactoryfeeSetter is the zero address.
InvalidFeeCollector()FactoryfeeCollector is the zero address.
InvalidRouter(address router)Adapter constructorrouter is the zero address.
InvalidTarget(address target)Adapter setAssetFeeA FeeConfig row has a zero bridgedToken.

Role handoff

The factory hands off the roles in this order:

  1. The adapter constructor grants DEFAULT_ADMIN_ROLE, FEE_SETTER_ROLE, and FEE_COLLECTOR_ROLE to the factory.
  2. The factory applies the configuration. setAssetFee needs FEE_SETTER_ROLE; the other calls need DEFAULT_ADMIN_ROLE.
  3. The factory grants DEFAULT_ADMIN_ROLE to defaultAdmin, FEE_SETTER_ROLE to feeSetter, and FEE_COLLECTOR_ROLE to feeCollector.
  4. The factory renounces FEE_COLLECTOR_ROLE, then FEE_SETTER_ROLE, then DEFAULT_ADMIN_ROLE.

Each role ends with exactly one holder, unless you pass the same account for several roles. Deploying the adapter with its constructor gives a different result:

RoleThrough the factoryThrough the constructor
DEFAULT_ADMIN_ROLEdefaultAdmindefaultAdmin
FEE_SETTER_ROLEfeeSetter onlyfeeSetter and defaultAdmin
FEE_COLLECTOR_ROLEfeeCollector onlyfeeCollector and defaultAdmin

DEFAULT_ADMIN_ROLE is the admin of both fee roles, so defaultAdmin can grant or revoke them later. The adapter uses OpenZeppelin AccessControlEnumerable, so grant, revoke, and renounce each take effect in one step, with no two-step transfer. If the last DEFAULT_ADMIN_ROLE holder renounces it, the adapter's configuration is locked permanently. Use a multisig for defaultAdmin.

Do not pass the factory's own address for any role. The factory renounces its roles in step 4, so that role would end with no holder.

Security notes

Permissionless deployment. Anyone can deploy an adapter that points at any router and any vault. An AdapterDeployed event from this factory does not show who controls the adapter. Before you trust an adapter, check its role holders with getRoleMember and its ROUTER.

Address derivation. The factory deploys with CREATE, so the adapter address depends on the factory address and its nonce. Each call to deploy from anyone increments the nonce. Read the address from the return value or the AdapterDeployed event instead of precomputing it. Do not assume an adapter has the same address on two chains.

No input validation beyond zero checks. The factory does not check that router has code or that vaultTarget is an ERC-4626 vault. The repository's pnpm ccip:deploy script checks both before it broadcasts. The router must have code, and the vault must return a non-zero asset(). ROUTER is immutable, so a wrong router means deploying a new adapter. A defaultAdmin you do not control also means deploying a new adapter, because nobody else can change the configuration.

Settings the factory does not apply. The factory does not configure CCIP 2.0 settings and does not fund the adapter. After deployment, the defaultAdmin account sets these on the adapter:

Anyone can also send the native gas token to the adapter, which pays the CCIP fee for return legs. A new adapter holds none. The repository's pnpm ccip:configure script applies these settings and the funding. For what each setting does and which lanes need it, see CCIP 2.0 lanes. For an example that sets the return format and funds the adapter, see Deploy the adapter.

Get the latest Chainlink content straight to your inbox.