> For the complete documentation index, see [llms.txt](https://docs.kpk.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpk.io/funds/infrastructure/deployment/cross-chain-deployment.md).

# Cross-chain deployment (CCIP)

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](/funds/infrastructure/architecture-overview.md)). Doing that by hand means running [`deployStack`](/funds/infrastructure/deployment.md) 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`](https://github.com/karpatkey/onchain-investment-vehicles/blob/main/src/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](/funds/infrastructure/deployment/deployment-addresses.md) |

***

## 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

<figure><img src="/files/nYHkG5pZ1v2xFebDIC6P" alt="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."><figcaption><p>One mainnet <code>deployEverywhere</code> call deploys the full OIV on mainnet and fans the operational stack out to each sidechain over CCIP — same addresses everywhere.</p></figcaption></figure>

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](#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). |

```solidity
function deployEverywhere(KpkOivFactory.OivConfig calldata config, uint256 gasLimit)
    external payable returns (KpkOivFactory.OivInstance memory instance, bytes32[] memory messageIds);

function deployEverywhere(KpkOivFactory.OivConfig calldata config, uint256[] calldata destChainIds, uint256 gasLimit)
    external payable returns (KpkOivFactory.OivInstance memory instance, bytes32[] memory messageIds);

function dispatchTo(KpkOivFactory.OivConfig calldata config, uint256[] calldata destChainIds, uint256 gasLimit)
    external payable returns (bytes32[] memory messageIds);

function quoteDeployEverywhere(KpkOivFactory.OivConfig calldata config, uint256 gasLimit)
    external view returns (uint256 totalFee, uint256[] memory feePerDestination);

function predictOiv(KpkOivFactory.OivConfig calldata config)
    external view returns (KpkOivFactory.OivInstance memory);
```

{% hint style="info" %}
**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`.
{% endhint %}

***

## 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](#operational-model)).

{% hint style="info" %}
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.
{% endhint %}

***

## 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`).

{% hint style="info" %}
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](/funds/infrastructure/core-concepts/roles-and-operators.md)).
{% endhint %}

***

## Operational model

{% hint style="warning" %}
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`.
{% endhint %}

* **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](https://ccip.chain.link).
* **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](#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](#ccip-fees).
* **`Empty` precondition.** `deployStack` reverts `EmptyContractMissing` unless the [`Empty` contract](/funds/infrastructure/deployment/deployment-addresses.md) 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](/funds/infrastructure/deployment/deployment-addresses.md) for the full list.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kpk.io/funds/infrastructure/deployment/cross-chain-deployment.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
