> 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/architecture/deployment.md).

# Fund deployment

How funds are created: the KpkOivFactory deploys the whole Safe + shares stack, and its governance timelocks, in one transaction, with deterministic addresses across chains.

Funds are not deployed by hand. A single onchain factory, **`KpkOivFactory`**, assembles the entire contract stack (Safes, Roles Modifiers, the shares token, and optional governance timelocks) in one transaction, fully wired and with ownership already transferred to the right parties.

## The factory

`KpkOivFactory` is deployed at the **same address on every supported chain**. Its deployment entry points are permissionless. The factory owner controls **only** a curated registry of externally deployed funds; it has no infrastructure setters.

| Entry point                | Deploys                                                                                                                                  | Used for                                               |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `deployStack(StackConfig)` | The **5-contract operational stack**: Portfolio Safe, Manager Safe, and the three Roles Modifiers, plus the exec timelock if configured. | Chains that hold the fund's assets but not its shares. |
| `deployOiv(OivConfig)`     | The **same stack plus** the fund's `kpkShares` ERC-1967 UUPS proxy, plus the shares timelock if configured.                              | Chains that carry the fund's shares token.             |

`deployOiv` has no chain restriction. A fund can carry shares on more than one chain, and its shares proxy lands at the **same address on each of them**, even where each uses a different base asset. For funds rolled out through the CCIP orchestrator, which chains carry shares is declared up front as the fund's **shares-chain topology** (see [Cross-chain deployment](/funds/infrastructure/architecture/deployment/cross-chain-deployment.md#shares-chains)).

A third entry point, `deployShares(OivConfig)`, adds the shares token to a chain that already has the fund's stack. It is what the orchestrator's `promoteShares` calls (see [Cross-chain deployment](/funds/infrastructure/architecture/deployment/cross-chain-deployment.md#adding-shares-to-a-new-chain)); use that rather than calling it directly.

### What `deployOiv` produces

{% stepper %}
{% step %}
**Portfolio Safe**: holds fund assets; sole owner is the `Empty` contract, so it can only be driven through the Roles Modifiers.
{% endstep %}

{% step %}
**Manager Safe**: operators' multisig; granted the `OPERATOR` role on the shares token.
{% endstep %}

{% step %}
**Exec / Sub / Manager Roles Modifiers**: the permission layer (see [Roles and operators](/funds/infrastructure/architecture/core-concepts/roles-and-operators.md)). The exec modifier is owned by `admin`, or by the fund's **exec timelock** when one is configured. The sub and manager modifiers are owned by the Manager Safe.
{% endstep %}

{% step %}
**`kpkShares` proxy**: the fund's ERC-20 shares token. Investors hold this. It points at the chain's **shared `kpkShares` mastercopy**; upgrades stay isolated per fund, because `upgradeToAndCall` writes only the calling proxy's own implementation slot. The factory initialises it, registers any additional assets, grants the Portfolio Safe → proxy allowances needed for redemptions, wires the Manager Safe as `OPERATOR`, hands `DEFAULT_ADMIN_ROLE` to `admin` (or to the **shares timelock** when one is configured) and renounces its own roles.
{% endstep %}

{% step %}
**Timelocks (optional)**: an **exec timelock** owning the exec Roles Modifier and a **shares timelock** holding `DEFAULT_ADMIN_ROLE` on the shares proxy, each deployed only when configured. See [Governance timelocks](#governance-timelocks).
{% endstep %}
{% endstepper %}

The deploying account **retains no privileged role** afterwards: all authority sits with `admin` (or its timelocks) and the Manager Safe configured at deploy time.

## Deterministic, cross-chain addresses

A single `salt` drives every CREATE2 deployment, with the caller's address mixed in to prevent salt-squatting. The consequence is the **cross-chain invariant**:

> For the same `(caller, salt)` on the same factory, `deployStack` and `deployOiv` produce **identical** Portfolio Safe, Manager Safe, and Roles Modifier addresses on **every** chain.

That is what lets a fund run `deployOiv` on its shares chains and `deployStack` everywhere else while keeping a **single Portfolio Safe address everywhere**. The exec timelock's address derives from the exec Roles Modifier's, so it too is identical on every chain. Two view functions return the addresses a deployment would produce, without sending a transaction:

* `predictStackAddresses(StackConfig, caller)` → the stack addresses (plus the exec timelock).
* `predictOivAddresses(OivConfig, caller)` → all of the fund's addresses.

{% hint style="info" %}
The same deployer account must be used across all chains to keep addresses identical.
{% endhint %}

The fund's **shares address binds its shares configuration**. The shares proxy's salt commits to every chain-invariant shares field: `admin`, name, symbol, both TTLs, `feeReceiver`, the three fee rates, `performanceFeeModule`, `additionalAssets`, and `sharesTimelock`. Correcting any of them after the fact moves the fund's canonical shares address. The per-chain **base asset** is deliberately excluded, which is what lets one shares address hold a different asset on each chain. Get the shares configuration right before the first deployment.

Running `deployStack` on each chain by hand is optional: the **`CcipOivDeployer`** orchestrator does the whole fan-out from a **single transaction on any wired chain** over Chainlink CCIP, keeping the same addresses everywhere. See [Cross-chain deployment (CCIP)](/funds/infrastructure/architecture/deployment/cross-chain-deployment.md).

## Reserved addresses and component adoption

A fund's contract addresses derive from its public configuration, so anyone can compute them before the fund deploys. Two of the factories involved, Safe's `SafeProxyFactory` and Zodiac's `ModuleProxyFactory`, are permissionless, so anyone can also create those contracts first, most likely on a chain the fund has not reached yet.

They cannot create *different* ones. The address is a function of the setup data, so whoever creates a component first must use the fund's own configuration, with the fund's operators as owners. The factory therefore **adopts** a pre-existing component after verifying that it matches the configuration its address encodes: for a Safe, the owners, threshold, exact module set, guard and fallback handler. Any mismatch is rejected. The one state it refuses outright is a fully wired stack, which reverts `StackAlreadyDeployedHere`. If the factory reverted on every pre-existing component instead, anyone could block a fund from a chain for the cost of gas, and the only recovery would be a configuration change that moves every address the fund has on every chain.

### Residual risk: the Manager Safe

The Roles Modifiers and the Portfolio Safe cannot have changed since creation. The Roles Modifiers are factory-owned with no modules enabled, so no one else can act on them. The **Portfolio Safe**'s only owner is the always-reverting `Empty` contract, so no signature for it can exist.

{% hint style="warning" %}
**The Manager Safe is an accepted risk, not an open defect.**

Its owners are the fund's operators, so a pre-created Manager Safe is a working multisig from the moment it exists. A Safe routes every call through a stored pointer to its implementation, and its signers can change that pointer, which is a supported Safe operation. Code behind a changed pointer can return exactly what the configuration expects to every check the factory makes, while behaving differently. **No onchain check detects this, including a check of the pointer itself**, because a contract can only inspect another contract by calling it, and the call runs whatever code the pointer designates.

Doing this requires **threshold-many Manager Safe signatures**. An outsider who creates the Safe at the reserved address cannot act, because the Safe's `setup` call fixed the owners to the fund's operators. Manager Safe owners must already be trusted at the same level as `admin` (see the trust assumptions under [Addresses](#addresses)), so this risk sits inside the existing trust model. Closing it onchain would reintroduce the denial of service described above.

**The control is offchain.** `eth_getStorageAt` reads the Manager Safe's implementation pointer directly from storage without executing contract code, so its result cannot be faked. Run this check before relying on an adopted Manager Safe; once it passes, the Safe's ordinary getters can be trusted. No deployment tooling runs it yet, so for now it is a manual step (tracked in [issue #58](https://github.com/karpatkey/onchain-investment-vehicles/issues/58)). Earlier factory generations revert on a pre-created Manager Safe, while salt v4 adopts it without an error, which is why the check is required. The adoption is recorded onchain only by `ComponentAdopted(component, kind)`, which the factory emits whenever it adopts a component instead of creating it (`kind` is `"safe"` or `"roles-modifier"`). `OivDeployed` looks the same either way, so index `ComponentAdopted` to detect an adoption. The storage slots, the required order of checks, and the checks that do not work are specified in [`DEPLOYMENT.md`](https://github.com/karpatkey/onchain-investment-vehicles/blob/main/DEPLOYMENT.md#adopting-a-pre-existing-safe) in the contracts repository.
{% endhint %}

Adopting a pre-existing `TimelockController` (see [Governance timelocks](#governance-timelocks)) has a comparable limit. OpenZeppelin's `AccessControl` is not enumerable, so an onchain check can confirm that the configured proposers and cancellers **hold** their roles, but not that no **additional** member exists. Funds built by the factory are not exposed to this: the timelock deployer mixes its caller's address into each timelock's salt, so a timelock that anyone other than the factory deploys lands at a different address and is never adopted. It applies only to a timelock you deploy yourself and later hand a Fund's control to.

{% hint style="info" %}
**None of the above applies to earlier factory generations (salt v3 and the legacy factory).** They have no adoption path: a component already present at a reserved address makes the deployment revert. No unverifiable adoption is possible, but anyone can deny a fund its reserved addresses on a chain it has not yet reached.
{% endhint %}

## Configuration

`deployOiv` takes an `OivConfig`. The shares parameters mirror the fund's economic settings (see [Fees](/funds/infrastructure/architecture/core-concepts/fees.md) and [Subscriptions](/funds/infrastructure/architecture/core-concepts/subscriptions.md)); fee rates are in **basis points** (100 bps = 1%), TTLs and delays in **seconds**.

```solidity
struct OivConfig {
    SafeConfig managerSafe;              // operator multisig: owners (strictly ascending) + threshold
    uint256 salt;                        // drives all deterministic addresses
    address admin;                       // exec-modifier owner + DEFAULT_ADMIN_ROLE on shares
                                         //   (each superseded by its timelock, if configured)
    KpkShares.ConstructorParams sharesParams;  // name, symbol, base asset, fees, TTLs, fee receiver
    AssetConfig[] additionalAssets;      // extra deposit/redemption assets beyond the base asset
    TimelockParams execTimelock;         // timelock owning the exec Roles Modifier (every chain)
    TimelockParams sharesTimelock;       // timelock holding DEFAULT_ADMIN_ROLE on the shares proxy
}

struct TimelockParams {
    uint256 minDelay;       // seconds; 0 = no timelock, otherwise 12 hours – 30 days
    address[] proposers;    // PROPOSER_ROLE (+ CANCELLER_ROLE); strictly ascending, ≤ 10
    address[] cancellers;   // CANCELLER_ROLE only (the veto); strictly ascending, ≤ 10
}

struct KpkShares.ConstructorParams {
    address asset;                  // base asset (e.g. USDC); auto-flagged isFeeModuleAsset = true
    address admin;                  // ignored; overridden by OivConfig.admin
    string  name;                   // share token name
    string  symbol;                 // share token symbol
    address safe;                   // ignored; overridden with the deployed Portfolio Safe
    uint64  subscriptionRequestTtl; // ≤ 7 days (larger values are capped, but still change the shares address)
    uint64  redemptionRequestTtl;   // ≤ 7 days (same)
    address feeReceiver;            // receives management/performance/redemption fee shares
    uint256 managementFeeRate;      // bps, ≤ 2000
    uint256 redemptionFeeRate;      // bps, ≤ 2000
    address performanceFeeModule;   // WatermarkFee module, or address(0) to disable
    uint256 performanceFeeRate;     // bps, ≤ 2000
}
```

The factory validates the config before deploying:

* **Manager Safe owners** must be non-zero and **strictly ascending by address value** (`OwnersNotAscending` otherwise), with a valid threshold. The owner array is hashed into the Manager Safe's address, so ascending order gives each signer set exactly one address on every chain.
* `admin` and the base asset must be non-zero; `feeReceiver` and both TTLs must be set; fee rates must be within bounds.
* `additionalAssets` must not repeat each other or the base asset, and each must be depositable, redeemable, or both.

{% hint style="warning" %}
**Governance addresses must work on every chain the fund reaches.** Whoever owns the exec Roles Modifier (`admin`, or the exec timelock's `proposers` and `cancellers`) acts on **every** chain the fund is deployed to, at the same address; `admin` (or the shares timelock's members) does the same on every shares chain. Use accounts that exist and are controlled at that address everywhere, normally a Safe deployed at the same address on every chain. Without an exec timelock, if `admin` is a Safe that exists on one chain only, the exec Roles Modifier on every other chain is owned by an address nobody controls there until that Safe is deployed there at the same address, where the canonical Safe setup allows it. Until then, nobody can call `promoteShares` on that chain. An exec timelock is itself deployed at one address on every chain, but its proposers still have to be reachable on each of them.
{% endhint %}

## Governance timelocks

A fund can put its two admin powers behind separate OpenZeppelin `TimelockController` instances, deployed by the factory at birth:

| Timelock         | Holds                                                     | Deployed on                                     |
| ---------------- | --------------------------------------------------------- | ----------------------------------------------- |
| `execTimelock`   | Ownership of the exec Roles Modifier (policy edits)       | **Every chain** the fund lives on, same address |
| `sharesTimelock` | `DEFAULT_ADMIN_ROLE` on the shares proxy (fees, upgrades) | Every chain that carries the fund's shares      |

* **`minDelay` of `0` means no timelock**: `admin` receives that authority directly. Any non-zero delay must be between **12 hours and 30 days** at deployment. After that, each timelock can change its own delay to any value, including below 12 hours, through a scheduled `updateDelay`; the change is per chain, and a chain the fund reaches later starts at the birth delay.
* **`proposers`** can schedule (and cancel) operations; **`cancellers`** can only veto. Entries must be non-zero addresses, **strictly ascending by address value**, and at most 10 per list. Execution is open to anyone once the delay has passed, and each timelock administers itself; the factory keeps no role on it.
* **Never list an address in both `proposers` and `cancellers`.** OpenZeppelin already grants every proposer `CANCELLER_ROLE`, so a repeat reverts `DuplicateRoleMember`. The usual pattern is a single proposer and no separate cancellers, for example the Security Council Safe as sole proposer with `cancellers: []`. The sole proposer should itself be a multisig: a timelock administers itself, so losing its only proposer freezes everything it governs, permanently.
* When a timelock is configured, it holds that authority **alone**. `admin` does not keep a parallel, delay-free path.
* There is deliberately **no floor** on either list. An empty `proposers` list produces a timelock that can never schedule anything, permanently freezing what it governs.

{% hint style="warning" %}
**Give a fund its timelocks at birth; don't retrofit them.** A fund's timelock configuration is part of its identity, and adding a shares chain later (`promoteShares`) depends on it:

* A fund born **with** an exec timelock can gain a new shares chain only while that **birth timelock still owns** the exec Roles Modifier.
* A fund born **without either timelock** refuses promotion after **any** change of exec governance, including a hand-deployed exec timelock, with `PromotionWouldRearmTheBirthAdmin`, because the new chain's shares admin role would otherwise go back to the replaced `admin`. A `sharesTimelock` configured at birth takes that role instead and lifts the restriction.

See [Adding shares to a new chain](/funds/infrastructure/architecture/deployment/cross-chain-deployment.md#adding-shares-to-a-new-chain).
{% endhint %}

## Addresses

`KpkOivFactory`, the `CcipOivDeployer` orchestrator, the shared `kpkShares` mastercopy, the timelock deployer and its mastercopy, and the `Empty` contract are deployed at the **same address on every supported chain** (20 chains):

| Contract                                   | Address                                      |
| ------------------------------------------ | -------------------------------------------- |
| `KpkOivFactory`                            | `0x73Bb12a05669748f3c9cbE8764271c69182f49E5` |
| `CcipOivDeployer`                          | `0xD99e4B13fc50A6321f6A84f2D4F83d6e34AE699D` |
| `KpkShares` mastercopy                     | `0x729Fb58a61a6f8349657fBc9f17BA4D36C9e72fC` |
| `KpkTimelockDeployer`                      | `0xdd23Ba8B2c4D3D916605361e29600121DeFC2d9f` |
| `TimelockControllerUpgradeable` mastercopy | `0x9760280fED9e760668186334f88b6d763A7d976E` |
| `Empty` (Portfolio Safe owner)             | `0xA4703438f8cc4fc2C2503a7e43935Da16BA74652` |

The full list of chains, ownership, verification, and the earlier factory generations existing funds run on is on the [Deployment addresses](/funds/infrastructure/architecture/deployment/deployment-addresses.md) page. Per-fund addresses are listed on each Fund's page, under Key information and Governance and controls (for example, [USD Alpha](/funds/funds/usd-alpha.md)).

{% hint style="warning" %}
**Trust assumptions.** The factory **owner** controls only the curated fund registry (`registerFund` / `unregisterFund`). Every infrastructure address (the Safe and Zodiac contracts, the `kpkShares` mastercopy, and the timelock deployer) is a **constructor argument with no setter**, so no owner can redirect a future deployment; a bad value needs a new factory generation. The owner should still be a governance multisig. Because deployment is permissionless, anyone can deploy a fund with arbitrary parameters, so verify a fund's `admin` and `managerSafe.owners` before trusting it. For a given fund, the **Manager Safe owners** receive ownership of the sub and manager Roles Modifiers, so they must be trusted at the same operational level as `admin`; the exec Roles Modifier (owned by `admin` or the exec timelock) remains the authoritative gatekeeper of Portfolio Safe execution.
{% endhint %}


---

# 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.kpk.io/funds/infrastructure/architecture/deployment.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
