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
deployreturns, the factory holds no role on the adapter and cannot change it. - The factory keeps no registry of adapters. The
AdapterDeployedevent 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 bytypeAndVersion. - 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
PUSH0opcode. Deploy them only on chains that support the Shanghai upgrade.
Deployed addresses
| Network | Address |
|---|---|
| Ethereum mainnet | 0x1b79038DCCbeE0E406eA0c1bA758A2cF696b38D4 |
| Ethereum Sepolia | 0x12A0f45738F3DDA931C4D3ECd1b267355e587921 |
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.
deployleaves the CCIP 2.0 settings at their defaults and does not fund the adapter. See Security notes.
Access: anyone.
Parameters:
| Parameter | Type | Description |
|---|---|---|
config | DeploymentConfig calldata | Router, role accounts, vault, processing switches, and batches of chain and fee rows. |
Returns:
| Name | Type | Description |
|---|---|---|
adapterAddress | address | Address of the deployed adapter. |
Reverts with:
InvalidAdmin,InvalidFeeSetter, orInvalidFeeCollectorwhen the matching role account is the zero address.InvalidRouter(address(0))from the adapter constructor whenconfig.routeris the zero address.InvalidTarget(address(0))from the adapter'ssetAssetFeewhen aFeeConfigrow has a zerobridgedToken.
Emits:
AdapterDeployedfrom the factory.- The adapter's own events from the configuration calls:
ChainTypeSetper chain row,TargetEnabledwhenvaultTargetis set,ProcessingEnabledSet, andAssetFeeSetper fee row. See the adapter's events. - OpenZeppelin
RoleGrantedandRoleRevokedevents from the adapter for the role handoff.
deploy runs these steps in order:
- Reverts if
defaultAdmin,feeSetter, orfeeCollectoris the zero address. These checks run before the adapter constructor, so a call with a zero router and a zero admin reverts withInvalidAdmin. - Deploys the adapter with
CREATE, passingconfig.routerand the factory's own address as admin, fee setter, and fee collector. The factory now holds all three roles. - Calls
setChainTypeonce for eachchainConfigsrow. - Calls
setTargetEnabled(vaultTarget, targetEnabled)only whenvaultTargetis not the zero address. Otherwise it skips the call and ignorestargetEnabled. - Calls
setProcessingEnabled(depositsEnabled, redeemsEnabled). This call always runs. - Calls
setAssetFeeonce for eachfeeConfigsrow. - Hands off the roles, as described in Role handoff.
- Emits
AdapterDeployedand 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 |
|---|---|---|
router | address | CCIP 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. |
defaultAdmin | address | Receives DEFAULT_ADMIN_ROLE. Must not be zero. |
feeSetter | address | Receives FEE_SETTER_ROLE. Must not be zero. |
feeCollector | address | Receives FEE_COLLECTOR_ROLE. Must not be zero. |
vaultTarget | address | ERC-4626 vault to allowlist. Pass the zero address to skip. |
targetEnabled | bool | Value passed to setTargetEnabled for vaultTarget. Ignored when vaultTarget is zero. |
depositsEnabled | bool | Whether the adapter processes deposits. |
redeemsEnabled | bool | Whether the adapter processes redemptions. |
chainConfigs | ChainConfig[] | Source chains to configure. Can be empty. |
feeConfigs | FeeConfig[] | Adapter fee rows. Can be empty. |
ChainConfig
| Field | Type | Description |
|---|---|---|
chainSelector | uint64 | CCIP chain selector of a source chain. |
chainType | CrossChainERC4626Adapter.ChainType | NONE (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 |
|---|---|---|
destinationChainSelector | uint64 | Chain the output returns to, which is the source chain of the inbound message. |
bridgedToken | address | Token bridged back, which is the share token (the vault address) for deposit returns and the asset for redeem returns. Must not be zero. |
fee | uint256 | Flat 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"
}
]
}
0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59is the CCIP Router on Ethereum Sepolia, and3478487238524512106is the Arbitrum Sepolia chain selector.- Replace
<your vault address>,<your vault asset address>, and the three role placeholders with your own addresses. - A
feeof1000000000000000is 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 | Indexed | Description |
|---|---|---|---|
adapter | address | Yes | Address of the new adapter. |
router | address | Yes | config.router. |
defaultAdmin | address | Yes | Account that received DEFAULT_ADMIN_ROLE. |
feeSetter | address | No | Account that received FEE_SETTER_ROLE. |
feeCollector | address | No | Account that received FEE_COLLECTOR_ROLE. |
vaultTarget | address | No | config.vaultTarget, or the zero address if none was set. |
targetEnabled | bool | No | config.targetEnabled, emitted even when vaultTarget is zero. |
depositsEnabled | bool | No | config.depositsEnabled. |
redeemsEnabled | bool | No | config.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:
| Error | Raised by | When |
|---|---|---|
InvalidAdmin() | Factory | defaultAdmin is the zero address. |
InvalidFeeSetter() | Factory | feeSetter is the zero address. |
InvalidFeeCollector() | Factory | feeCollector is the zero address. |
InvalidRouter(address router) | Adapter constructor | router is the zero address. |
InvalidTarget(address target) | Adapter setAssetFee | A FeeConfig row has a zero bridgedToken. |
Role handoff
The factory hands off the roles in this order:
- The adapter constructor grants
DEFAULT_ADMIN_ROLE,FEE_SETTER_ROLE, andFEE_COLLECTOR_ROLEto the factory. - The factory applies the configuration.
setAssetFeeneedsFEE_SETTER_ROLE; the other calls needDEFAULT_ADMIN_ROLE. - The factory grants
DEFAULT_ADMIN_ROLEtodefaultAdmin,FEE_SETTER_ROLEtofeeSetter, andFEE_COLLECTOR_ROLEtofeeCollector. - The factory renounces
FEE_COLLECTOR_ROLE, thenFEE_SETTER_ROLE, thenDEFAULT_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:
| Role | Through the factory | Through the constructor |
|---|---|---|
DEFAULT_ADMIN_ROLE | defaultAdmin | defaultAdmin |
FEE_SETTER_ROLE | feeSetter only | feeSetter and defaultAdmin |
FEE_COLLECTOR_ROLE | feeCollector only | feeCollector 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:
- the return format for each CCIP 2.0 lane, with
setEvmReturnLaneFormat - the requested finality of return legs, with
setEvmReturnRequestedFinality, if you change the default - the inbound finality, with
setInboundFinality, if you change the default - the Cross-Chain Verifier (CCV) policy, with
setCCVsConfig, if the lane's default verifiers are not enough
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.
Related contracts
- Adapter contract: the
CrossChainERC4626Adapterthatdeploycreates ITypeAndVersionIAny2EVMMessageReceiverV2: the receiver interface the adapter implementsExtraArgsCodec: encodes theGenericExtraArgsV3return format- OpenZeppelin
AccessControlEnumerable: role management on the adapter