For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

At a glance

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

Deployment 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

A single mainnet deployEverywhere call: CcipOivDeployer deploys the full OIV on mainnet and, via the CCIP router and network, delivers to the sibling orchestrator on each sidechain, which deploys the operational stack at the same addresses.
One mainnet 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.

Function
Purpose

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 configsalt = 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.value of the deployEverywhere / dispatchTo call — 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 ccipReceive there up to the gasLimit passed. 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 the totalFee and the per-destination breakdown. Send at least totalFee as msg.value.

  • Surplus is refunded. If msg.value exceeds 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. gasLimit is the destination execution budget baked into each message's price (deployStack measures ~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:

  1. msg.sender is the configured CCIP Router.

  2. message.sourceChainSelector is the configured Ethereum-mainnet selector.

  3. 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

  • Partial failure is possible. A destination message can fail (e.g. gas underestimate, or a missing Empty contract on that chain). It then enters CCIP's FAILED state and can be manually re-executed within its retry window. Monitor delivery on the CCIP Explorer.

  • Recovery / add-a-chain. deployEverywhere is the first, atomic fan-out and cannot be re-run with the same config (its local deployOiv would collide on CREATE2 addresses). To extend a fund to a new sidechain — or resend to one whose delivery permanently failed — use dispatchTo with the same config (notably the same salt), 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 gasLimit makes the destination deployStack run out of gas and the message enter FAILED (re-executable) — size it as in CCIP fees.

  • Empty precondition. deployStack reverts EmptyContractMissing unless the Empty contract 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