Cross-chain deployment (CCIP)
How a single mainnet transaction deploys a fund on mainnet and fans the operational stack out to sidechains over Chainlink CCIP — same addresses everywhere.
A fund spans one or more chains, and its Portfolio Safe, Manager Safe, and Roles Modifiers must share the same address on every chain (see Architecture overview). Doing that by hand means running deployStack on each sidechain from the same account, in separate transactions. CcipOivDeployer automates it: a single mainnet transaction deploys the full OIV on mainnet and fans the operational stack out to every configured sidechain over Chainlink CCIP — producing identical Portfolio / Manager / Roles addresses across all chains.
Source: CcipOivDeployer.sol
Contract
CcipOivDeployer (external orchestrator around KpkOivFactory)
Address
0x6F2A3D35Ff275d6B76dB47eFB0Da1b2358daf11b — same on every chain
Source chain
Ethereum mainnet (SOURCE_CHAIN_ID = 1)
Entry points
deployEverywhere · dispatchTo (permissionless, caller-funded)
Addresses
Why an orchestrator (not CCIP inside the factory)
KpkOivFactory mixes msg.sender into every CREATE2 salt to stop salt-squatting, so its cross-chain address invariant holds only when the same caller invokes the factory on every chain. A raw CCIP integration would break this — on a destination chain the factory's caller would be the CCIP Router, not the original mainnet account.
CcipOivDeployer solves it by being the single, uniform caller of the factory on every chain. Because the orchestrator is itself deployed at the same address on all chains (deterministic CREATE2, identical creation code), the factory sees one identical msg.sender everywhere, and the address invariant is preserved — with no CCIP logic inside the factory's deployment path. All CCIP, fee, and router logic lives in the orchestrator.
The flow
deployEverywhere call deploys the full OIV on mainnet and fans the operational stack out to each sidechain over CCIP — same addresses everywhere.The mainnet leg deploys the full OIV (the 5-contract operational stack plus the kpkShares token) via deployOiv. Each sidechain leg deploys the operational stack only via deployStack, at the same addresses — the shares token lives on one chain, while every chain holds a matching Portfolio Safe for bridged portfolio assets. The orchestrator derives each sidechain's StackConfig by calling the factory's own oivToStackConfig(config) helper at runtime, so the mapping can never drift and fragment a fund's addresses.
Entry points
Both write entry points are permissionless and caller-funded (fees paid in native gas as msg.value — see CCIP fees), and run only on the source chain (mainnet), reverting NotSourceChain elsewhere.
deployEverywhere(config, gasLimit)
Deploy the full OIV on mainnet and fan out to all configured destination chains.
deployEverywhere(config, destChainIds[], gasLimit)
Same, but only to the given destination chains.
dispatchTo(config, destChainIds[], gasLimit)
CCIP fan-out only (no local deployOiv) — to add a chain later or retry a permanently-failed delivery.
quoteDeployEverywhere(config, [destChainIds[],] gasLimit)
View: total native fee and per-destination breakdown to size msg.value.
predictOiv(config)
View: the addresses a deployment of config would produce (applies the orchestrator's salt derivation — see below).
Config-bound salt. The orchestrator is the factory's uniform caller, which would neutralise the factory's caller-mixed anti-squat salt. To restore it, the orchestrator derives the salt from the whole config — salt = keccak256(abi.encode(config)). Any config difference (notably admin) changes every deployed address, so an attacker cannot land a fund at another config's addresses; an identical config still yields identical addresses on every chain. Off-chain tooling must predict via predictOiv(config), not the factory's raw predictOivAddresses.
CCIP fees
Every cross-chain message costs a CCIP fee. deployEverywhere sends one message per destination chain, so the amount to pay is the sum of the per-destination fees.
Paid in native gas, by the caller. The fee is paid in the source chain's native token (ETH on mainnet) as the
msg.valueof thedeployEverywhere/dispatchTocall — the account that triggers the deployment funds it. CCIP is not paid in LINK here, and the orchestrator holds no balance, so there is no shared treasury to pre-fund or drain.What a fee covers. For each destination, the CCIP Router prices delivering the message and executing
ccipReceivethere up to thegasLimitpassed. The total therefore scales with the number of destinations and the gas limit.Quote before you send.
quoteDeployEverywhere(config, [destChainIds,] gasLimit)is a view that returns thetotalFeeand the per-destination breakdown. Send at leasttotalFeeasmsg.value.Surplus is refunded. If
msg.valueexceeds the total fee, the excess is refunded to the caller in the same transaction — so sending a small buffer over the quote is safe.The gas limit is prepaid in the fee.
gasLimitis the destination execution budget baked into each message's price (deployStackmeasures ~1.45M gas — pass ~1.8M–2.0M; CCIP caps destination execution at 3M). It is prepaid, so unspent destination gas is not refunded, and too low a limit makes the destination delivery fail (see Operational model).
No LINK pre-funding or fee treasury is required. The CcipDeployEverywhere deploy script calls quoteDeployEverywhere and forwards the result as msg.value automatically, with a small buffer.
Security model
ccipReceive accepts a message only when all three hold:
msg.senderis the configured CCIP Router.message.sourceChainSelectoris the configured Ethereum-mainnet selector.The decoded source sender equals
address(this)— the sibling orchestrator on mainnet (same address everywhere).
Check (3) blocks a forged message from pre-occupying a salt's deterministic CREATE2 addresses and griefing the legitimate deployment. deployEverywhere / dispatchTo are restricted to the source chain so a permissionless caller cannot run the full deployOiv directly on a destination chain and pre-occupy the stack addresses (which would make the later CCIP deployStack collide and stick in FAILED).
The orchestrator never holds a privileged role on any deployed fund. The exec Roles Modifier — owned by config.admin — remains the authoritative gatekeeper of Portfolio Safe execution (Roles and operators).
Operational model
Cross-chain delivery is asynchronous, not atomic. The mainnet transaction confirms once the messages are dispatched; each sidechain stack materialises later (after Ethereum finality, ~15 min) when CCIP delivers to ccipReceive.
Partial failure is possible. A destination message can fail (e.g. gas underestimate, or a missing
Emptycontract on that chain). It then enters CCIP'sFAILEDstate and can be manually re-executed within its retry window. Monitor delivery on the CCIP Explorer.Recovery / add-a-chain.
deployEverywhereis the first, atomic fan-out and cannot be re-run with the same config (its localdeployOivwould collide on CREATE2 addresses). To extend a fund to a new sidechain — or resend to one whose delivery permanently failed — usedispatchTowith the sameconfig(notably the samesalt), so the stack lands at the fund's existing addresses. Never re-dispatch to a chain that already has the stack.Fees. Paid by the caller in native gas as
msg.value; quote and size them per CCIP fees.Gas limit. Too low a
gasLimitmakes the destinationdeployStackrun out of gas and the message enterFAILED(re-executable) — size it as in CCIP fees.Emptyprecondition.deployStackrevertsEmptyContractMissingunless theEmptycontract is predeployed on the target chain — ensure this first.New funds only. Addresses are keyed to the orchestrator, so a fund previously deployed directly by an EOA cannot be retro-extended through this path; a fund using CCIP must enter through the orchestrator from the start.
Supported destinations
The mainnet orchestrator's selector registry covers every deployed sidechain (each chain except mainnet itself). The orchestrator, factory, and Empty contract are live on the same chains — see Deployment addresses for the full list.
Last updated