# Builder start

> Build on IPOR Fusion: the pages an agent needs to deploy, configure and operate a Fusion vault (Plasma Vault) with the ipor-fusion Python SDK, in the order of the deploy path: install, factory proxy, clone, roles, substrates, balance fuse, deposit, execute, common errors. The full text of each page follows the list.

Every other docs page is listed in https://ipor.io/docs/llms.txt (all pages inlined: https://ipor.io/docs/llms-full.txt). The main IPOR Fusion index is https://ipor.io/llms.txt.

## Deploy path

1. [SDK](https://ipor.io/docs/build-on-fusion/alpha/sdk.md): Install, connect and execute with the Python SDK; the CLI, MCP server, agent skills, examples and module reference.
2. [Deploy your first Fusion vault](https://ipor.io/docs/build-on-fusion/developer-guide/deploy-your-first-fusion-vault.md): The executed eleven-transaction SDK walk to clone, configure and fund a Fusion vault from scratch.
3. [Addresses](https://ipor.io/docs/build-on-fusion/developer-guide/addresses.md): Canonical IporFusionFactoryProxy addresses per chain, verified against the ipor-abi registry.
4. [Common Errors](https://ipor.io/docs/build-on-fusion/developer-guide/common-errors.md): Revert selectors and the role table for the errors most likely to block a Fusion vault deployment.
5. [What is a Fuse?](https://ipor.io/docs/build-on-fusion/architecture-overview/what-is-a-fuse.md): What a Fuse is, how it adapts vault calls to an external protocol, and the fuse whitelist registry.
6. [Balance Fuses](https://ipor.io/docs/build-on-fusion/developer-guide/balance-fuses.md): What a Balance Fuse tracks and how it pairs with a Functional Fuse for a market.
7. [Substrates](https://ipor.io/docs/build-on-fusion/atomists/vault-configuration-step-by-step/substrates.md): What substrates are and how they scope which assets and markets a fuse is allowed to touch.
8. [Access Management](https://ipor.io/docs/build-on-fusion/atomists/vault-configuration-step-by-step/access-management.md): How OpenZeppelin Access Manager governs vault roles, and how to grant, revoke and time-lock them.
9. [MCP](https://ipor.io/docs/build-on-fusion/developer-guide/mcp.md): Connect to the hosted Fusion MCP server: tools, resources, prompts and a worked example for inspecting vaults.

---

Source: https://ipor.io/docs/build-on-fusion/alpha/sdk.md

# SDK

> Install, connect and execute with the Python SDK; the CLI, MCP server, agent skills, examples and module reference.

The official Python SDK for IPOR Fusion (`pip install ipor-fusion`; [PyPI](https://pypi.org/project/ipor-fusion/), [source](https://github.com/IPOR-Labs/ipor-fusion.py)). Use it to create vaults, construct contract calls, encode calldata and submit transactions through a configured signer — the only one of Fusion's public interfaces that can write, not just read (see MCP and the API for read-only surfaces).

For the full first-vault walkthrough, see Deploy your first Fusion vault. For revert selectors and the role table, see Common errors.

> **Note:** This page describes SDK 4.0. Upgrading from 3.x? 4.0 breaks 3.x code at run time: wrappers are built from an address alone, every execution takes the context (`.call(ctx)`, `.send(ctx)`), methods that read the chain to answer are `read_*(ctx, ...)`, and a block is pinned with `ctx.at_block(n)`. Follow the ordered checklist in [MIGRATION_4.md, _Start here_](https://github.com/IPOR-Labs/ipor-fusion.py/blob/main/MIGRATION_4.md#start-here).

### Install

```bash
pip install ipor-fusion    # Python 3.11+
```

### Connect and execute

```python
from ipor_fusion import (
    AaveV3SupplyFuse,
    Amount,
    PlasmaVault,
    Web3Context,
    parse_fuse_address,
    parse_plasma_vault_address,
)
from web3 import Web3

# 1. Create a Web3 context with your provider and private key
ctx = Web3Context.from_url(
    url="https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY",
    private_key="0x...",
)

# 2. Wrap the PlasmaVault contract (an address only; the context is passed at execution)
vault = PlasmaVault(parse_plasma_vault_address("0xVAULT_ADDRESS"))

# 3. Build a fuse action (e.g. supply USDC to Aave V3)
fuse = AaveV3SupplyFuse(parse_fuse_address("0xFUSE_ADDRESS"))
action = fuse.supply(
    asset=Web3.to_checksum_address("0xUSDC_ADDRESS"),
    amount=Amount(1_000_000),  # 1 USDC (6 decimals)
)

# 4. Execute on-chain (execute() returns a Call; .send(ctx) signs and submits it)
receipt = vault.execute([action]).send(ctx)
```

Fuse, factory and manager addresses per chain are published in [`ipor-abi`](https://github.com/IPOR-Labs/ipor-abi) (`mainnet/mainnet-<chain>-fusion/addresses.json`) — see also Addresses; `fetch_address_registry(chain_id).address("SupplyFuseAaveV3")` reads them by name from the SDK. A fuse must also be registered on the vault; `vault.get_fuses().call(ctx)` lists the registered ones. Amounts are raw on-chain integers (`Amount`, `Shares`); the SDK scales by decimals only in explicit boundary converters: `parse_amount("1.5", Decimals(6))` and `format_amount(Amount(1_500_000), Decimals(6))` convert exactly, with no floats and no rounding (`parse_shares` / `format_shares` for shares).

Ids, addresses and amounts are nominal types (`ChainId`, `MarketId`, `Period`, `Amount`, `PlasmaVaultAddress`, `FuseAddress`, ...), all importable from `ipor_fusion`. At run time they are the plain value (`ChainId(8453)` is `8453`), so code that is not type-checked needs no wrapping; a type checker rejects a bare `int` or `str` where the SDK expects one of them. Wrap literals (`ChainId(8453)`, `Period(0)`, `Amount(n)`) and parse raw addresses once (`parse_plasma_vault_address`, `parse_fuse_address`, `parse_access_manager_address`); values the SDK returns already carry their type. The `parse_*` address parsers are strict: normalize user input first, `parse_plasma_vault_address(Web3.to_checksum_address(raw.strip()))`.

### Read, send, or simulate

Wrapper methods that encode Solidity calls return a `Call` instead of executing immediately; composite `read_*` methods execute reads with the context you pass them. Wrappers and `Call`s hold no `Web3Context`: the context is a required argument wherever something runs. The same `Call` powers all three modes — this is the SDK's read-only-vs-transaction-sending surface:

```python
from ipor_fusion import VaultSimulator

total = vault.total_assets().call(ctx)          # eth_call -> Amount, no key needed
receipt = vault.execute([action]).send(ctx)     # signed tx -> TxReceipt, needs a private key
payload = vault.execute([action]).calldata      # raw bytes for an external signer

# Simulate first via eth_simulateV1 (no local node); alpha is the account allowed to call execute()
sim = VaultSimulator(ctx.web3, vault=vault.address, alpha=Web3.to_checksum_address("0xALPHA"))
before = sim.observe(vault.total_assets())      # SimulationObservation[Amount]
sim.execute([action])
after = sim.observe(vault.total_assets())
result = sim.run()
if result.all_success:
    print(after.value - before.value)
```

`.send(ctx)` signs locally and requires a private key in the `Web3Context`; `.call(ctx)` and `.calldata` need none — sending through a keyless context raises `ValueError`. `.build_transaction(ctx)` prepares an unsigned transaction for an external signer and `.estimate_gas(ctx)` estimates its gas.

Methods that must read the chain to answer (storage, log scans, a read that depends on another read) are named `read_*` and take the context first, for example `vault.read_balance_fuses(ctx)` or `AccessManager(manager).read_all_role_accounts(ctx)`. To read at one block, pin a copy of the context: `ctx.at_block(n)`; the original is not changed. Independent reads go in one `eth_call` through a typed batch:

```python
from ipor_fusion import Multicall3

pinned = ctx.at_block(ctx.web3.eth.block_number)
batch = Multicall3(pinned).batch()
assets = batch.add(vault.total_assets())    # BatchValue[Amount]
fuses = batch.add(vault.get_fuses())        # BatchValue[list[FuseAddress]]
name = batch.try_add(vault.name())          # BatchValue[str | None]: None if the read fails
batch.run()
print(assets.value, fuses.value, name.value)
```

For an existing vault, `preflight_execute` checks a prepared execute batch as its sender would send it: the sender's permission (or a ready scheduled operation for this exact calldata), fuse registration, balance fuses for every touched market and every market its balance dependencies reach, and, by default, one `eth_simulateV1` run. It does not broadcast, and a pass is evidence for the state tested, not a promise for the block the transaction lands in. `simulate=False` runs only the static checks.

```python
from ipor_fusion import preflight_execute

check = preflight_execute(
    ctx, vault.address, [action],
    sender=Web3.to_checksum_address("0xALPHA_ADDRESS"),
)
check.raise_for_failure()
```

Read a batch back from calldata, e.g. to review a multisig transaction. `decode_fuse_action` decodes with the encoder class you choose; `fuse_classes_for` lists only the selector-compatible candidates, not the fuse deployed at that address:

```python
from ipor_fusion import decode_execute_calldata, decode_fuse_action, fuse_classes_for

# The class behind each fuse address you know (from the registry or your config);
# fuse_classes_for(action) lists the candidates when you do not.
known = {fuse.address: AaveV3SupplyFuse}
for decoded_action in decode_execute_calldata(payload):  # execute / claimRewards calldata
    cls = known.get(decoded_action.fuse)
    if cls is None:
        print(decoded_action.fuse, "one of", [c.__name__ for c in fuse_classes_for(decoded_action)])
        continue
    decoded = decode_fuse_action(decoded_action, cls)
    print(decoded.label, decoded.args)  # AaveV3SupplyFuse.enter ((asset, amount, e_mode),)
```

### CLI: `fusion`

The SDK ships a `fusion` CLI for **inspecting** Plasma Vaults from the terminal — it does not deploy, configure or execute; that's the SDK's job above.

```bash
# Install with CLI extras (pipx keeps dependencies isolated)
pipx install 'ipor-fusion[cli]'

# Configure an RPC provider (auto-detects chain ID)
fusion config set-provider https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY

# Inspect a vault (auto-saves to config on first use)
fusion vault info 0xB8a451107A9f87FDe481D4D686247D6e43Ed715e --chain-id ethereum

# Validate a vault: ok / warning / critical verdict, exit status 1 on critical
fusion vault validate 0xB8a451107A9f87FDe481D4D686247D6e43Ed715e --chain-id ethereum

# List saved vaults
fusion vault list

# Check an execute batch before signing it (role, fuses, balance fuses, simulation)
fusion vault preflight 0xVAULT_ADDRESS --calldata 0xEXECUTE_CALLDATA --sender 0xALPHA --chain-id base

# Inspect a Morpho Blue market or MetaMorpho vault
fusion market morpho-blue 0xMARKET_ID --chain ethereum
fusion market meta-morpho 0xVAULT_ADDRESS --chain ethereum

# Find IPOR Fusion contracts in the ipor-abi registry (no RPC provider needed)
fusion address lookup SupplyFuseAaveV3 --chain base
fusion address names --chain base
fusion address list --chain base
```

Chain and block arguments reject `0`: omit an optional one for auto-detection or the latest block.

### MCP: `fusion-mcp`

Two ways to reach Fusion from an MCP-compatible client — see MCP for the hosted server's full tool/resource/prompt reference.

- **Hosted, no install:** `https://mcp.ipor.io/mcp` — public, unauthenticated, read-only.
- **Local, from the SDK:** `pipx install 'ipor-fusion[mcp]'` ships a `fusion-mcp` server that exposes the same CLI over MCP, against your own RPC providers and local config, plus the resources `fusion://glossary`, `fusion://architecture`, `fusion://invariants` and `fusion://quickstart`, and the prompts `quickstart`, `deploy_vault`, `analyze_vault`, `trace_oracle_pricing` and `explain_fuse`.

```json
{
  "mcpServers": {
    "ipor-fusion": { "command": "fusion-mcp", "type": "stdio" }
  }
}
```

The local server's tools mirror the CLI, among them `vault_validate`, `vault_preflight`, `address_lookup`, `address_names` and `address_list`; the hosted server names its address tools `fusion_address_lookup`, `fusion_address_names` and `fusion_addresses_list`.

### Agent skills

[`skills/ipor-deploy-vault/SKILL.md`](https://github.com/IPOR-Labs/ipor-fusion.py/blob/main/skills/ipor-deploy-vault/SKILL.md) teaches a coding agent the full clone -> roles -> market -> access posture -> deposit -> execute walk, with the invariants and the revert selectors, before it writes vault code — the same text as `fusion://invariants` / `fusion://quickstart` and Common errors, one text with several delivery paths. One install per machine:

```bash
# Claude Code -- plugin with the skill and the hosted MCP server
/plugin marketplace add IPOR-Labs/ipor-fusion.py
/plugin install ipor-fusion@ipor-fusion

# Codex CLI, Gemini CLI, Cursor -- the skill; add the MCP server as shown above
npx skills add IPOR-Labs/ipor-fusion.py -g -a codex      # or gemini-cli, cursor
```

### Vault construction examples

Runnable, canonical examples for building and configuring a vault from scratch live in [`examples/`](https://github.com/IPOR-Labs/ipor-fusion.py/tree/main/examples). They preview and simulate through `eth_simulateV1` — nothing is ever signed or broadcast.

- [Simple Aave V3 supply vault](https://github.com/IPOR-Labs/ipor-fusion.py/blob/main/examples/simple_aave_v3_supply_base.py) — start here for vault creation, role bootstrap, and configuring a single supported market.
- [Advanced Euler V2 credit-market vault](https://github.com/IPOR-Labs/ipor-fusion.py/blob/main/examples/advanced_euler_v2_credit_market_base.py) — continue here for composing multiple functional fuses, typed Euler substrates and sub-accounts, and an ordered collateral -> borrow -> repay -> unwind lifecycle.

For a strategy bot that operates an existing vault end to end, see [`ipor-fusion-alpha-example`](https://github.com/IPOR-Labs/ipor-fusion-alpha-example).

### Architecture

The SDK uses a **fuse adapter pattern**: Fuses encode protocol-specific calls into `FuseAction` objects (pure calldata, no state); `PlasmaVault` batches and executes `FuseAction` sequences on-chain via `execute()` (atomically); `Web3Context` manages provider connections, signing and transaction dispatch; `Call` holds an encoded contract call: `.call(ctx)`, `.send(ctx)`, `.calldata`, or feed it to `VaultSimulator`.

```
Fuse.method()  -->  FuseAction  -->  PlasmaVault.execute([actions])  -->  Call  -->  .send(ctx) / simulate
```

Stable names are exported through `ipor_fusion.__all__`: import them from `ipor_fusion`, its public subpackages (`core`, `config`, `fuses`, `readers`), or the public module paths listed in the README's [API policy](https://github.com/IPOR-Labs/ipor-fusion.py#public-api-and-stability). Submodules of a public subpackage, such as `ipor_fusion.core.contract`, are internal.

> **Warning:** Crosschain (market 54) and HyperCore (market 55) run on production contracts; their SDK API is in preview. They import only from `ipor_fusion.crosschain` and `ipor_fusion.hypercore`, never from the top level. The SDK follows only their official deployment (the 2026-10-21 contracts release, listed in [`ipor-abi`](https://github.com/IPOR-Labs/ipor-abi)) and nothing earlier, and their interfaces may change or be removed between minor versions, with no backward compatibility. Pin an exact SDK version if you depend on them.

#### Core modules (`ipor_fusion.core`)

| Module                                                   | Purpose                                                                                                                                                                                                        |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Web3Context`                                            | Provider connection, signing, tx dispatch, gas estimation                                                                                                                                                      |
| `Call`                                                   | Pre-encoded contract call; `.call(ctx)`, `.send(ctx)`, `.calldata`, `.build_transaction(ctx)`                                                                                                                  |
| `Multicall3`                                             | Batch independent reads into one `eth_call` (`batch()`, `aggregate`, `try_aggregate`)                                                                                                                          |
| `PlasmaVault`                                            | ERC-4626 vault — execute, deposit, withdraw, fuse and market configuration                                                                                                                                     |
| `VaultSimulator`                                         | Batch `execute` + reads through `eth_simulateV1`, multi-block, no local node                                                                                                                                   |
| `FusionFactory`                                          | Deploy a new vault (`clone`, `clone_supervised`)                                                                                                                                                               |
| `AccessManager`                                          | Roles (`grant_role`, `can_call`), redemption locks (`get_account_lock_time`) and timelocks (`schedule`, `execute`, `cancel`); `read_call_plan(ctx, call, caller=...)` names the next step for a delayed caller |
| `RewardsManager`                                         | Claim and vest rewards                                                                                                                                                                                         |
| `WithdrawManager`                                        | Time-windowed withdrawal requests                                                                                                                                                                              |
| `FeeManager`                                             | Deposit, performance, and management fee configuration                                                                                                                                                         |
| `PriceOracleMiddleware` / `PriceOracleMiddlewareManager` | Asset price feeds and per-vault overrides                                                                                                                                                                      |
| `ERC20`                                                  | Token reads and approvals                                                                                                                                                                                      |

`PlasmaVault` also builds the governance calls: price oracle, fees, rewards claim manager, share transfers, minimal execution delays for roles and market limits. Market limits are `Wad` fractions of total assets, not token amounts: `setup_markets_limits([(IporFusionMarkets.AAVE_V3, Wad(6 * 10**17))])` caps the market at 60%, takes effect once `activate_markets_limits()` is on, and a limit outside `0..10**18` raises before encoding.

For a caller with an execution delay, `AccessManager.read_call_plan` answers `immediate`, `schedule`, `waiting`, `ready` or `forbidden`, with `next_call` when there is a transaction to send. It does not plan calls on the access manager itself and does not run the target's own checks (redemption lock, pre-hooks, balances): simulate the call for those.

#### Supported protocols (`ipor_fusion.fuses`)

Aave V3, Morpho, Euler V2, Uniswap V3, Ramses V2, Compound V3, Gearbox V3, ERC-4626, Fluid Instadapp, Merkl, plus a Universal Token Swapper fuse. Full class list in the [README](https://github.com/IPOR-Labs/ipor-fusion.py#supported-protocols-ipor_fusionfuses).

#### Supported networks

The on-chain vault tooling is validated on the eleven chains in `ipor_fusion.SUPPORTED_CHAIN_IDS`: Ethereum, Arbitrum, Base, Unichain, Plasma, Avalanche, Katana, Monad, HyperEVM, Ink and Robinhood. Ink and Robinhood have no live vault yet; they are validated on a vault freshly cloned through their FusionFactory on a fork. Read the set from the SDK rather than hard-coding it:

```python
from ipor_fusion import CHAIN_NAMES, SUPPORTED_CHAIN_IDS

for chain_id in sorted(SUPPORTED_CHAIN_IDS):
    print(chain_id, CHAIN_NAMES[chain_id])
```

### Development and contributing

Contributor and coding-agent instructions (commands, conventions, invariants, domain rules) live in [`AGENTS.md`](https://github.com/IPOR-Labs/ipor-fusion.py/blob/main/AGENTS.md) in the repository.


---

Source: https://ipor.io/docs/build-on-fusion/developer-guide/deploy-your-first-fusion-vault.md

# Deploy your first Fusion vault

> The executed eleven-transaction SDK walk to clone, configure and fund a Fusion vault from scratch.

The path from nothing to a vault with a live position is eleven transactions, all signed by the account that will own the vault. There is no shortcut in the SDK: every one of them is mandatory. The MCP invariants resource (`fusion://invariants`) says what reverts when a step is skipped. Copy the walk below and change only the addresses.

### Install

```bash
pip install ipor-fusion                                     # SDK 4.0 (Python 3.11+)
curl -L https://foundry.paradigm.xyz | bash && foundryup    # anvil, for fork tests
anvil --fork-url <BASE_RPC_URL> --chain-id 8453             # then point the SDK at :8545
```

The hosted read-only server at `https://mcp.ipor.io/mcp` and the bundled `fusion-mcp` server inspect vaults; neither deploys, configures, deposits or executes. Those go through the SDK below.

### The walk: clone, roles, market, access posture, deposit, execute

Base, USDC into Aave V3. Executed end to end on a Base fork with this SDK: all eleven transactions land, the vault's idle USDC goes from 0 to the deposit through `deposit()`, and its aToken balance rises after `execute`. Swap the four addresses for another chain or market (see Addresses); keep the order.

```python
import os

from eth_account import Account
from web3 import Web3

from ipor_fusion import (
    ERC20,
    AccessManager,
    Amount,
    ChainId,
    FusionFactory,
    IporFusionMarkets,
    Period,
    PlasmaVault,
    Roles,
    Web3Context,
    parse_fuse_address,
)
from ipor_fusion.fuses import AaveV3SupplyFuse

# A Base RPC, or an anvil fork of Base: anvil --fork-url <BASE_RPC_URL> --chain-id 8453
w3 = Web3(Web3.HTTPProvider("http://localhost:8545"))
OWNER_PRIVATE_KEY = os.environ["OWNER_PRIVATE_KEY"]  # .send(ctx) signs locally; .call(ctx) previews need no key
owner = Account.from_key(OWNER_PRIVATE_KEY).address  # owns, configures and operates the vault
ctx = Web3Context(w3, chain_id=ChainId(8453), signer=owner, private_key=OWNER_PRIVATE_KEY)

# Generic addresses are ChecksumAddress; fuse addresses carry their role (FuseAddress).
FACTORY_PROXY = Web3.to_checksum_address("0x1455717668fA96534f675856347A973fA907e922")  # IporFusionFactoryProxy, Base
USDC = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
SUPPLY_FUSE = parse_fuse_address("0x26fD6EF391E98C78CfCA27e00c3d15be4D941625")  # SupplyFuseAaveV3, Base
BALANCE_FUSE = parse_fuse_address("0xf53f3EaFfDf67539256365cA7299540A98b60BA9")  # BalanceFuseAaveV3, Base

# 1. Deploy, then read the addresses actually created from the mined receipt.
#    Do not use a prior .call(ctx) preview: another clone can consume those CREATE
#    addresses before this transaction lands.
factory = FusionFactory(FACTORY_PROXY)
clone = factory.clone(
    asset_name="My Vault",
    asset_symbol="MV",
    underlying_token=USDC,
    redemption_delay_seconds=Period(0),
    owner=owner,
)
receipt = clone.send(ctx)
instance = factory.decode_clone_receipt(receipt)
vault_address = instance.plasma_vault
access_manager_address = instance.access_manager

# 2. Roles. The clone grants `owner` only OWNER_ROLE. ATOMIST first: it administers the rest.
am = AccessManager(access_manager_address)
am.grant_role(Roles.ATOMIST_ROLE, owner, Period(0)).send(ctx)
am.grant_role(Roles.FUSE_MANAGER_ROLE, owner, Period(0)).send(ctx)
am.grant_role(Roles.ALPHA_ROLE, owner, Period(0)).send(ctx)

# 3. Market, in this order: fuses -> substrates -> balance fuse.
vault = PlasmaVault(vault_address)
vault.add_fuses([SUPPLY_FUSE]).send(ctx)
usdc_substrate = bytes(12) + bytes.fromhex(USDC[2:])  # address right-aligned in bytes32
vault.grant_market_substrates(IporFusionMarkets.AAVE_V3, [usdc_substrate]).send(ctx)
vault.add_balance_fuse(IporFusionMarkets.AAVE_V3, BALANCE_FUSE).send(ctx)

# 4. Access posture. A fresh clone is private and deposit() reverts until one of these.
am.grant_role(Roles.WHITELIST_ROLE, owner, Period(0)).send(ctx)  # (a) own bot: whitelist the depositor
# vault.convert_to_public_vault().send(ctx)              # (b) outside money: ATOMIST-only, one-way

# 5. Fund and run the first strategy step. The depositor must already hold the USDC.
amount = Amount(1_000 * 10**6)  # raw units: 1,000 USDC at 6 decimals
ERC20(USDC).approve(vault_address, amount).send(ctx)
vault.deposit(amount, owner).send(ctx)
vault.execute([AaveV3SupplyFuse(SUPPLY_FUSE).supply(asset=USDC, amount=amount)]).send(ctx)
```

Notes on the walk:

- `clone` versus `clone_supervised`: same arguments; the supervised variant is gated by a maintenance role. Use `clone`.
- The balance fuse above is the `BalanceFuseAaveV3` entry of the registry for Base and is verified working on a fresh clone. Vaults already in production may register a different one for the same market; to match them, read `PlasmaVault(address).read_balance_fuses(ctx)` on such a vault.
- Substrate layouts are market-specific. For Aave V3 the substrate is the asset address right-aligned in 32 bytes; other markets use typed layouts, decoded by `ipor_fusion.substrates` and documented in the fuse libraries of the [contracts repository](https://github.com/IPOR-Labs/ipor-fusion).
- Wrappers hold only an address; the context goes to `.call(ctx)` / `.send(ctx)`. Ids, durations and amounts are typed (`ChainId(8453)`, `Period(0)`, `Amount(...)`, `parse_fuse_address(...)`): plain values at run time, checked by a type checker. Code written for SDK 3.x (`PlasmaVault(ctx, address)`, `.send()` without a context) fails at run time under 4.0; see [MIGRATION_4.md](https://github.com/IPOR-Labs/ipor-fusion.py/blob/main/MIGRATION_4.md#start-here).
- The four addresses come from the `ipor-abi` registry: `fetch_address_registry(ChainId(8453))` returns them by name (`.address("IporFusionFactoryProxy")`, `.address("SupplyFuseAaveV3")`, `.address("BalanceFuseAaveV3")`), as does `fusion address lookup <name> --chain base`. The lookup needs no RPC provider or key, and it does not show that a fuse is registered on your vault.
- Give the bot its own key: grant `ALPHA_ROLE` to the bot's address instead of `owner`, and keep the owner key offline.

Before step 5's `execute`, or any later batch, check the batch as its sender would send it. The check uses `eth_simulateV1` by default (a provider that supports it); `simulate=False` runs only the static checks. It sends no transaction, so the walk stays eleven transactions:

```python
from ipor_fusion import preflight_execute

action = AaveV3SupplyFuse(SUPPLY_FUSE).supply(asset=USDC, amount=amount)
preflight_execute(ctx, vault_address, [action], sender=owner).raise_for_failure()
```

### Creation is not configuration

Creating a vault deploys the Fusion vault infrastructure; it does not make the vault strategy-ready. See Vault configuration step-by-step for the full configuration surface (price oracle, scheduled withdrawals, fees, and more) beyond the minimal market setup shown above. Market limits take `Wad` fractions of total assets (`Wad(6 * 10**17)` for 60%), not token amounts. Roles granted with an execution delay must schedule their calls: `AccessManager.read_call_plan` names the next step before you send a restricted vault call.

### Pointers

- Factory proxy addresses for every supported chain: Addresses
- Fuse addresses by name and chain: [`ipor-abi`](https://github.com/IPOR-Labs/ipor-abi), read from the SDK with `fetch_address_registry(chain_id)`, the CLI's `fusion address lookup`, or the hosted MCP server's `fusion_address_lookup`
- The SDK runs this exact walk in one `eth_simulateV1` batch: `tests/test_simulate_vault_from_scratch_base.py` in [`ipor-fusion.py`](https://github.com/IPOR-Labs/ipor-fusion.py)
- A strategy bot that operates an existing vault: [`ipor-fusion-alpha-example`](https://github.com/IPOR-Labs/ipor-fusion-alpha-example)


---

Source: https://ipor.io/docs/build-on-fusion/developer-guide/addresses.md

# Addresses

> Canonical IporFusionFactoryProxy addresses per chain, verified against the ipor-abi registry.

Canonical per-chain deployment addresses for the IPOR Fusion vault factory. This table is the source you should resolve `IporFusionFactoryProxy` from before following Deploy your first Fusion vault — never hardcode an address from a test or an example without checking it here first.

### Factory proxies per chain

`clone()` goes to the proxy column. The implementation column is listed only so you recognise and avoid it — calling it directly reverts `DaoFeePackagesArrayEmpty()`. Verify against [`ipor-abi`](https://github.com/IPOR-Labs/ipor-abi) before use; the registry is the source of truth and is regenerated on release.

| Chain     | Chain id | `IporFusionFactoryProxy` (use)               | `IporFusionFactoryImpl` (reverts)            | Vault tooling | Verified            |
| --------- | -------- | -------------------------------------------- | -------------------------------------------- | ------------- | ------------------- |
| Ethereum  | 1        | `0xcd05909C4A1F8E501e4ED554cEF4Ed5E48D9b852` | `0x14d120C365Debd920eaDf0a7AC31e659Bc58217b` | validated     | implementation slot |
| Flare     | 14       | `0xE75F3565D67645775351A9886F2b598f07d1860B` | `0x6C1ff6eBf5e92Dbc5591E7cCF455829De9C46579` | factory only  | implementation slot |
| Unichain  | 130      | `0xC9b4E839e86284C558791C13Bda97999fc49aF63` | `0x73c7CA802403BB21e9608C937c556C04d5eeBAD6` | validated     | implementation slot |
| Monad     | 143      | `0x62DFfe8bCaaf7960f858a4f7dDC83cc98F04cEDC` | `0x1017f0032460B3c8FBB92Bfd350F708Ff6229137` | validated     | implementation slot |
| TAC       | 239      | `0x26b5561E2E0e7d7977431E92740903Ef3cB7194b` | `0x7DDd0bBB59eFB4b7791cE5E1Cf1587F083aE4Ef5` | factory only  | implementation slot |
| HyperEVM  | 999      | `0x6bBc827F34b4e862d15215ABd4AC0E3886181665` | `0x010430cbdE2Bb8ACC48D7aDfbEb99812E83f1156` | validated     | implementation slot |
| Botanix   | 3637     | `0x7c9857f22D0cc6523C8e88BFDCbf411AB8300fF5` | `0xddb0EFB2a7a5C02e01b5A1f85C0a1110d68EeC26` | factory only  | registry only       |
| Robinhood | 4663     | `0xDF7A590b60072476E355A8EfF33e4872440bDc7C` | `0x9d897beA78F8747d8f9C45d5734089ca7225AE90` | validated     | implementation slot |
| Base      | 8453     | `0x1455717668fA96534f675856347A973fA907e922` | `0x378fEb2E96082FB965E2BB922AEE484d07c14014` | validated     | implementation slot |
| Plasma    | 9745     | `0x70d41759FBF90fB63c88197217cB08F93a1c65CB` | `0x346DA190c60d6101B77DA613a252c7aE25A7a120` | validated     | implementation slot |
| Arbitrum  | 42161    | `0x134fCAce7a2C7Ef3dF2479B62f03ddabAEa922d5` | `0x86C8Ea8800EB6bc8C846c3e0230178520a0ED721` | validated     | implementation slot |
| Avalanche | 43114    | `0xa00b6379833D77fA9C9497b32dae00dF39Ac751e` | `0x0f358Bb3d14AB911dBfAF4d9061097e7F5Cf123F` | validated     | implementation slot |
| Ink       | 57073    | `0xEC53f69Bd1D991a2F99e96DE66E81D0E42A61D8D` | `0xEF774B9767cC9FC813A2b595BE870A13D5B367C0` | validated     | implementation slot |
| Katana    | 747474   | `0xc29b8D591d6a3f109Ca7ba384F2e00162866D37B` | `0x4a4De6e86AD0d0546F55B595cA75f55803D940bA` | validated     | implementation slot |

A deployed factory is not the same as supported tooling. "Validated" chains are `SUPPORTED_CHAIN_IDS` (`ipor_fusion.chains`): `fusion vault info`/`validate` and `fetch_vault_data` run there. On a "factory only" chain `clone()` works, but the vault tooling raises `UnsupportedChainError` until the chain is validated. "Implementation slot" means the proxy's EIP-1967 implementation slot was read on chain and matches the registry's `IporFusionFactoryImpl`; "registry only" means the pair comes from `ipor-abi` without an on-chain read. The factory implementations are upgraded in place, so the implementation column changes with releases while the proxy stays.

### Fuse addresses

Fuse addresses are not in this table — they are market- and chain-specific (for example `SupplyFuseAaveV3` and `BalanceFuseAaveV3` on Base). Resolve them by registry name and chain from [`ipor-abi`](https://github.com/IPOR-Labs/ipor-abi): `fetch_address_registry(ChainId(8453)).address("SupplyFuseAaveV3")` in the SDK, `fusion address lookup SupplyFuseAaveV3 --chain base` in the CLI, or the MCP tool `address_lookup` (the hosted server's `fusion_address_lookup`). The SDK class for a registry name swaps the parts: `SupplyFuseAaveV3` is `AaveV3SupplyFuse`.


---

Source: https://ipor.io/docs/build-on-fusion/developer-guide/common-errors.md

# Common Errors

> Revert selectors and the role table for the errors most likely to block a Fusion vault deployment.

Revert selectors and setup errors an agent or developer is likely to hit while deploying or operating a Fusion vault (see Deploy your first Fusion vault), with the cause and the fix for each. The SDK README's Common errors and the MCP resource `fusion://invariants` carry the same deployment checks.

### Revert table

| Selector                                                    | Revert                               | What happened                                                                                                                                                                                                                             | Fix                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0x8745fbfd`                                                | `DaoFeePackagesArrayEmpty()`         | `clone()` was sent to `IporFusionFactoryImpl`                                                                                                                                                                                             | Send it to `IporFusionFactoryProxy` for that chain — see Addresses                                                                                                                                                                                                                                                                                                                                                            |
| `0x9996b315`                                                | `AddressEmptyCode(address)`          | `execute()` touched a market with no balance fuse                                                                                                                                                                                         | `add_balance_fuse(market_id, balance_fuse)` before the first `execute` on that market                                                                                                                                                                                                                                                                                                                                         |
| `0x4bf4de4e`                                                | `HandlerNotFound()`                  | A protocol called back into the vault during `execute()` (a flash loan) and no handler is registered for its address and selector                                                                                                         | `update_callback_handler(handler, sender, selector)` before the first `execute` that triggers the callback                                                                                                                                                                                                                                                                                                                    |
| `0x068ca9d8`                                                | `AccessManagedUnauthorized(address)` | The caller lacks the role the function requires: `FUSE_MANAGER` for `add_fuses`, `grant_market_substrates`, `add_balance_fuse`, `update_callback_handler`; `ALPHA` for `execute`; `WHITELIST` for `deposit` and `mint` on a private vault | Check first with `AccessManager.can_call(caller, target, selector).call(ctx)` (a closed target or a missing role answers `immediate=False, delay=0`; a role with an execution delay answers its `delay`). `AccessManager.grant_role(role, account, Period(0))` from the role's admin (`OWNER` grants `ATOMIST`, `ATOMIST` grants the rest); for a reverting `deposit`, whitelist the depositor or convert the vault to public |
| `ValueError: Private key required for sending transactions` | SDK, before any transaction          | `.send(ctx)` on a `Web3Context` without a key                                                                                                                                                                                             | `Web3Context(w3, chain_id, signer=..., private_key=...)` or `Web3Context.from_url(url, private_key=...)`                                                                                                                                                                                                                                                                                                                      |

A positive `can_call(...).call(ctx).delay` means the call must be scheduled, not that another role grant is missing: `AccessManager.read_call_plan(ctx, call, caller=...)` tells a missing schedule from a waiting or a ready one. For withdrawals, redemptions and share transfers also read `get_account_lock_time(account).call(ctx)`: a deposit or mint locks its caller, which can differ from the share receiver. Neither check replaces a simulation.

For an execute batch, `fusion vault preflight <vault> --calldata <hex> --sender <account> --chain-id base` (SDK: `preflight_execute`, MCP: `vault_preflight`) checks the permission, the registered fuses, the balance dependencies and a simulated execution together. Market limits take `Wad` fractions of total assets: `Wad(6 * 10**17)` is 60%, and a token amount such as `1_000_000` sets a near-zero ceiling; a limit outside `0..10**18` is rejected before encoding.

### Role table

| Role                | ID  | Grants                                                                                         | Granted by                                                   |
| ------------------- | --- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `OWNER_ROLE`        | 1   | Everything below, via the access manager                                                       | Set at vault creation from the `owner` argument to `clone()` |
| `ATOMIST_ROLE`      | 100 | Vault configuration authority; administers `FUSE_MANAGER`, `ALPHA` and `WHITELIST`             | `OWNER`                                                      |
| `ALPHA_ROLE`        | 200 | Can call `execute`                                                                             | `ATOMIST`                                                    |
| `FUSE_MANAGER_ROLE` | 300 | Can call `add_fuses`, `grant_market_substrates`, `add_balance_fuse`, `update_callback_handler` | `ATOMIST`                                                    |
| `WHITELIST_ROLE`    | 800 | Can `deposit` / `mint` into a private vault                                                    | `ATOMIST`                                                    |

A fresh clone grants the owner only `OWNER_ROLE` — every configuration and `execute` call from that account reverts `AccessManagedUnauthorized` until `ATOMIST_ROLE` is granted first (it administers the rest), matching step 2 of Deploy your first Fusion vault.

### Related invariants

- Clone from `IporFusionFactoryProxy`, never from `IporFusionFactoryImpl` — only the proxy carries the fee configuration.
- Register all three of `add_fuses`, `grant_market_substrates` and `add_balance_fuse` before the first `execute` on a market. Any order between them works; what is not optional is that all three precede `execute`.
- A fresh clone is private: `deposit` and `mint` revert until either the depositor holds `WHITELIST_ROLE` (keeps the vault private) or an Atomist calls `convert_to_public_vault()` (opens deposits to every address, and cannot be reverted).
- A flash-loan fuse also needs `update_callback_handler(handler, sender, selector)` before its first `execute`: Morpho Blue calls back `onMorphoFlashLoan(uint256,bytes)`, the Euler EVC `onEulerFlashLoan(bytes)`, and an unregistered callback reverts `HandlerNotFound()`.
- Fuses are immutable and addresses are per chain — resolve `(chain, name)` in [`ipor-abi`](https://github.com/IPOR-Labs/ipor-abi); never reuse an address across chains.


---

Source: https://ipor.io/docs/build-on-fusion/architecture-overview/what-is-a-fuse.md

# What is a Fuse?

> What a Fuse is, how it adapts vault calls to an external protocol, and the fuse whitelist registry.

> Fuses are one of three pillars on which Fusion stands. Fuses are the remedy providing permissionlessness, transparency, security, and speed of development.

**Fuses** are small contracts that are attached to vaults to perform atomic actions on integrated protocols.

All Fuses must meet the following requirements:

- **Stateless** — They have no memory, no configuration, and they store no data. Thanks to that they can be used by multiple vaults at the same time without the risk of interfering with one another.
- **Non-upgradable** — Fuses, by nature, are simple contracts that can not be altered. That assures a Plasma Vault’s actions can be trusted and Fuses can be shared without additional risk. Should the destination market’s ABI change, a new Fuse needs to be deployed.\*
- **Standard ABI** — One of the tasks of the Fuse is to bring the interface of the market down to a standard ABI. Fuses implement two core functions: Enter and Exit that, depending on context deploy or withdraw assets, take a loan or repay it, etc. On top of that some Fuses can implement instantWithdraw — a function with predefined params to pull the assets from the market without the need for Alpha’s interaction. This is relevant if the Vault allows for instant redemptions of shares.
- **Configurability** — Because Fuses are stateless, any configuration is delegated to the vault. The Fuse is built to accept the calls with the configuration.

## Types of Fuses

Fuses, by design, are limited to specific uses. One protocol may allow many use cases such as providing liquidity, trading derivatives, staking, etc. Outside of functions that can be performed, there are other kinds of actions that need to be performed such as checking balance or claiming incentives. Below is the detailed list of kinds of fuses used by Fusion.

### Functional fuses

These bi-directional fuses allow for action and its reversion. What that means depends on the protocol but some examples would include:

- **Provide** and **withdraw** liquidity from a DEX or a Credit Market
- **Borrow** against collateral and **repay** the loan
- **Open** and **close** a derivative such as an option, an interest rate swap, or a perpetual future

### Balance Fuses

Each project may require slightly different steps to price assets that are deposited into it by the vault. Vault may be depositing liquidity and at the same time borrowing against collateral. Balance fuse provides a standard way to account for the balance that is then taken when calculating the exchange of the vault's shares. When supporting a given protocol, a balance fuse is the mandatory fuse to provide. See also [here](https://ipor.io/docs/build-on-fusion/developer-guide/balance-fuses.md).

### Rewards Fuses

Each Vault comes standard with a Rewards module. It is responsible for claiming the rewards on behalf of the Vault and allows to pass the claimed incentives to a dedicated agent for processing. If the rewards are sold, the proceeds can be deposited back to the rewards manager and vested into the vault. Claiming of the rewards is done via a specialized fuse, that when invoked, will handle the logic of claiming the rewards.

### Swap Fuses

Swapping in the context of Fusion is not reversible. Therefore the swap fuses have slightly different interfaces. They allow for the swapping of two approved assets.

### Custom Fuse

Besides the typical fuses listed above, custom fuses can be created and used by the vault. Since fuses follow a delegate call, the fuse custom function can also be executed. This can extend the functionality of the vault to accommodate new workflows. It is important to note that custom fuses must be thoroughly tested, and possibly audited before use.

## Developing a Fuse

The Fusion DAO is dedicating efforts to provide lots of quality fuses. However, if you would like to see a fuse developed that is not on the DAO's backlog, you may want to develop it yourself. If you're the protocol administrator and wish Fusion to integrate with you, this is the fastest way to integrate. You can find detailed instructions on the [dedicated section](https://ipor.io/docs/build-on-fusion/integrating-a-new-market.md).

### Currently available fuses developed by IPOR Labs

The readme of below repository has the latest list of deployed fuses <https://github.com/IPOR-Labs/ipor-abi>

## Fuse Whitelist Registry

The IPOR DAO maintains a global Fuse Whitelist Registry that tracks the status of all Fuses (`ACTIVE`, `DEPRECATED`, `REMOVED`).

The Whitelist is [here](https://app.ipor.io/fusion-terminal/fuse-whitelist/ethereum/fuses) available in Fusion Terminal.


---

Source: https://ipor.io/docs/build-on-fusion/developer-guide/balance-fuses.md

# Balance Fuses

> What a Balance Fuse tracks and how it pairs with a Functional Fuse for a market.

#### Short Summary

**Balance fuses shift accounting and validation from opaque, manual, and mutable off-chain spreadsheets to small, auditable, onchain modules that read vault state and market data, enforce whitelists/constraints, and produce verifiable outputs.** That architectural change gives permissionless verification, immutable audit trails, and automated enforcement — properties that Excel cannot provide.

---

> How do Balance Fuses in IPOR Fusion work, and why do they offer more security than Excel spreadsheets in offchain accounting (as seen in other protocols like Stream Finance)?

IPOR Fusion’s **balance fuses** are a concrete example of how onchain, composable guards can replace fragile off-chain bookkeeping (like Excel) and give vaults provable, auditable accounting and behavior. Below is explained what they are, how they work (at a technical level you can audit), and exactly why that is materially more secure than maintaining off-chain spreadsheets.

## What a “balance fuse” is (short)

A **balance fuse** in IPOR Fusion is an onchain, market-specific fuse module whose job is to _determine and/or validate balances for a market or integration_ and to enforce constraints the vault relies on when routing assets through external protocols. Balance fuses are one type of “fuse” in the Fusion architecture — small, purpose-built, immutable onchain components that the vault calls when performing actions.

## How they work — mechanics you can audit

1. **Onchain interface** — balance fuses implement a defined interface (e.g. `IMarketBalanceFuse`) exposing functions such as a market-balance query (get user/market balance in USD or shares). That means external actors (and auditors) can call and inspect exactly how balances are derived.
2. **Vault-level accounting remains authoritative** — The fuse itself _never holds funds or persistent accounting state_; the vault remains the single source of truth for tokens/shares. The fuse is a verification/adapter layer that reads vault state, reads market/substrate data (oracles, onchain positions), and returns validated numbers or allows/blocks actions. This reduces complexity and centralizes accounting to an auditable smart contract. It allows also for instant withdrawals (if configured for the vault) with accurate share valuation.
3. **Market-specific logic & whitelisting** — a balance fuse is written for a particular market/substrate and can include checks like allowed assets, allowed method signatures, and validation constraints. Vaults can be configured only to accept [whitelisted](https://ipor.io/docs/build-on-fusion/architecture-overview/what-is-a-fuse.md)/verified fuses and markets, and non-whitelisted custom fuses trigger alerts in the Fusion UI. That restricts what external integrations can do.
4. **Price oracles & middleware tie-ins** — balance fuses integrate with Fusion’s price oracle middleware and market substrate configs so the numeric balances they return are grounded in onchain price feeds and standardized market identifiers (not an off-chain spreadsheet cell someone updated).
5. **Immutable, auditable code** — fuses are immutable (non-upgradable) pieces of code: you can review their logic onchain or in the project repo and reason about exactly how a balance was computed. Every call and event is logged on the blockchain.

## Why this is more secure than Excel/off-chain accounting (concrete comparisons)

**1) Immutability vs. silent edits**

- Excel: a spreadsheet cell can be changed, copied, or deleted without an immutable audit trail. A malicious or accidental edit can silently change reported balances.
- Balance fuse: logic lives onchain and is immutable (or at least upgrade paths are explicit and auditable). Any change requires a transaction and is visible onchain.

**2) Single source of truth vs. manual reconciliation**

- Excel: reconciliation requires manual imports/exports from multiple protocols and human consolidation — error-prone and inconsistent.
- Fusion: the vault is the authoritative contract; fuses read the vault and onchain markets directly and return validated numbers. No manual patching of “what the ledger says.”

**3) Permissioned behavior vs. ad-hoc scripts**

- Excel: operators often use ad-hoc scripts or spreadsheets to decide swaps/allocations; those scripts can call unsafe contracts or be manipulated.
- Balance fuses: constrain what external protocols/methods are allowed (whitelists, allowed signatures) and can refuse actions that would break rules. Non-whitelisted fuses trigger UI alerts.

**4) Onchain verifiability vs. opaque off-chain proofs**

- Excel: even with csv exports, a counterparty or auditor must trust that the exported file was correct; proving the file maps to onchain state is extra work.
- Balance fuse: anyone can call the same onchain functions, re-derive balances from onchain data, and verify results match what the vault used — cryptographically verifiable.

**5) Event logs & monitoring vs. manual logs**

- Excel: change history depends on systems (file history, emails); detection of tampering or mistakes is slow.
- Balance fuse + vault: every call, rebalancing, or state change emits events onchain. The system can emit alerts when a non-whitelisted fuse is used or when constraints are violated, enabling automated monitoring.

## Practical examples (what a balance fuse prevents)

- A bad agent tries to report inflated collateral or misprice an Illiquid token in a spreadsheet to justify an unsafe leverage step → onchain fuse uses oracle data and vault accounting and rejects the action.
- A manager wants to route funds into a non-whitelisted market with custom call signatures → vault/fuse whitelists prevent the call or trigger alarms before funds move.


---

Source: https://ipor.io/docs/build-on-fusion/atomists/vault-configuration-step-by-step/substrates.md

# Substrates

> What substrates are and how they scope which assets and markets a fuse is allowed to touch.

In IPOR Fusion, the Atomist defines the "walled garden" in which the strategy logic (Alpha) can operate. While Fuses define _how_ to interact with a protocol (e.g., "supply to Aave"), Substrates define _where_ and with _what_ specifically those Fuses are allowed to interact.

#### What are Substrates?

Substrates are specific identifiers or permissioned parameters granted to a Market within a Fusion Vault. They act as the granular configuration that tells the vault which underlying components of an external protocol are "visible" and "authorized."

Think of a Market (like Aave V3) as a building. The Fuse is the door you use to enter. Substrates are the specific rooms inside that building that the Atomist has unlocked for the Alpha.

Common examples of Substrates include:

- Asset Addresses: Specific ERC20 tokens allowed for supply/borrow.
- Sub-Market IDs: Specific pool IDs or market identifiers within a larger protocol (e.g., a specific Morpho Blue market).
- Target Limits: Constraints on asset amounts or specific function selectors allowed for execution.

#### Why are Substrates necessary?

Substrates serve two primary purposes:

1. Security: They prevent an Alpha from interacting with unauthorized, risky, or unverified pools within a protocol.
2. Accounting: The Plasma Vault uses Substrates to know which balances it needs to track. If a Substrate isn't configured, the vault won't "see" the assets held in that sub-market, ensuring the vault's Net Asset Value (NAV) and share price remain accurate and protected.

#### Configuration Step-by-Step

As an Atomist, you configure Substrates after adding a Fuse to your vault. This is done via the `grantMarketSubstrates` method.

**1. Identify the Market ID**

Every external protocol integration in Fusion has a unique `uint256` Market ID defined in the `IporFusionMarkets` library.

- _Example:_ `AAVE_V3 = 1`, `COMPOUND_V3_USDC = 2`, `MORPHO_BLUE = 5`.

**2. Define the Substrate Data**

The data required for a substrate varies depending on the Market type.

- For Lending Markets (Aave, Spark, etc.): The substrate is typically the address of the underlying asset (e.g., the USDC token address).
- For Complex Markets (Morpho Blue, Uniswap V3): The substrate might be a specific Market ID or a pool address.
- For Async Actions: Substrates can include encoded limits (`ALLOWED_AMOUNT_TO_OUTSIDE`) or permitted function selectors (`ALLOWED_TARGETS`).

**3. Grant Permissions**

Using the Vault's management interface (or directly via the `PlasmaVault` contract), the Atomist calls:

`grantMarketSubstrates(uint256 marketId, bytes[] calldata substrates)`

This registers the list of authorized IDs for that specific market.

#### Technical Example: Morpho Blue

If you are setting up a vault to supply USDC to a specific Morpho Blue market, you wouldn't just grant access to "Morpho." You would:

1. Identify the Morpho Market ID (the unique hash for the specific collateral/loan pair).
2. Add this ID as a Substrate to the Morpho Blue Market in your vault.
3. The vault now knows to track the balance of the `Id` and allows the Alpha to call `enter` or `exit` for that specific pool.

#### Summary for Atomists

- Fuses = Actions (Supply, Withdraw, Swap).
- Markets = Protocols (Aave, Morpho, Gearbox).
- Substrates = Specifics (USDC, Market ID `0x123...`, Function selectors).

Without the correct Substrates, your Alpha will not be able to execute trades, even if the Fuse is correctly installed.

---

#### Related Resources

- [Video: Explaining Substrates (YouTube)](https://www.google.com/search?q=https://youtube.com/watch%3Fv%3DzYXwfeUp7Og)


---

Source: https://ipor.io/docs/build-on-fusion/atomists/vault-configuration-step-by-step/access-management.md

# Access Management

> How OpenZeppelin Access Manager governs vault roles, and how to grant, revoke and time-lock them.

## Overview

IPOR Fusion utilizes OpenZeppelin's AccessManager to restrict function access through a role-based system. The roles below are ordered from highest to lowest authority. (Developer note: The list of roles is available in the GitHub repository: <https://github.com/IPOR-Labs/ipor-fusion/blob/main/contracts/libraries/Roles.sol>)

![Image](https://ipor.io/docs/assets/build-on-fusion/screenshot-2025-06-05-at-2-37-40-pm-a6bee3.png)

__

![Image](https://ipor.io/docs/content-images/08856daef7ac.jpg)

__

## Grant Delays (The Waiting Room)

When [timelocks](https://ipor.io/docs/build-on-fusion/atomists/vault-configuration-step-by-step/timelocks-and-execution-delays.md) are enabled, adding a new account to an important role follows a strict procedure to prevent the sudden injection of foreign keys into governance.

When a new account is granted an important role, its membership does not activate immediately — it waits out that role's grant delay first (recommended: 14 days for `OWNER`, 3 days for the configuration tier). This closes the "inject a key into governance" path: even a grant that completes successfully produces an account that cannot act yet.

The rule itself is protected too. Changing a grant delay via `setGrantDelay` arms only after a hardcoded 5-day minimum setback (`minSetback`) — and reducing one takes the larger of 5 days and the size of the reduction. Nobody, including a legitimate administrator, can shorten the waiting room and walk a new key straight in. Note that grant delays are `ADMIN`-gated: they must be configured before `ADMIN` is renounced, or they become permanently unavailable.

_Developer Note:_ Re-granting an existing member — for example raising their execution delay — is not subject to the grant delay. This is deliberate: it keeps hardening instantaneous.

## Admin Role

An account with this role has the rights to manage the IporFusionAccessManager in general. It is the highest role, capable of managing all roles including `ADMIN_ROLE` and `OWNER_ROLE`.

This is a technical role used during the bootstrapping phase of a vault. In a fully secured, timelocked vault deployment, the `ADMIN` role is ultimately **renounced**. _Note:_ When a vault is created in supervised mode, the `ADMIN` role must be **manually renounced** (after making the `OWNER` self-administered) to remove the "skeleton key" that could override all security mechanisms instantly.

## Owner Role

The highest administrative role, defined during the vault's bootstrapping process. The account with this role has rights to manage Owners, Guardians, Atomists, and configure execution delays (timelocks).

- Self-managed (if `getRoleAdmin(OWNER) == OWNER`, the Owner can change itself to another Owner)
- Single user (recommended use of multisig for this role)

**Best practices**

- **14-Day Timelock:** The Owner role should be configured with a 14-day execution delay to ensure all top-level administrative changes are publicly visible well in advance.
- **Strict Key Separation:** Use a highly secure, dedicated multisig wallet. The accounts holding the Owner role must be strictly separated from accounts holding operational roles (like Atomist or Alpha) and must be completely distinct from Guardian keys.
- **Cold Wallet Usage:** Treat the Owner multisig effectively as a cold wallet used only for structural vault changes.

## Atomist Role

This role serves as the primary managing accounts for the vault, overseeing most day-to-day settings, such as configuring Fuse Managers, Alphas, fees, and other parameters.

- Configurable by the Owner
- Allows multiple users

#### Best practices

- Use multiple Atomists to ensure redundancy.
- Apply timelocks to selected Atomist actions whenever feasible.

## Alpha

The account with this role has rights to execute the strategy on the vault using execute method, typically as an automated strategy manager.

- Configurable by the Atomist
- Allows multiple users

Depending on the level of automation, the following roles can be assigned to the same wallet managed by Alpha in the IPOR Fusion system:

### CONFIG_INSTANT_WITHDRAWAL_FUSES_ROLE (900)

This role configures fuses, such as credit market deposit fuses, to enable instant withdrawals, allowing liquidity providers (LPs) to withdraw assets from the vault immediately. The role holder defines the order of configured markets to ensure asset availability for LPs. A key requirement is that the market (a collection of fuses linked to an external platform) must be fully liquid from the vault’s perspective. If liquidity is used as collateral for borrowing, instant withdrawals could trigger liquidation, so such markets should not be used. For complex vault strategies involving adding or removing credit positions, assign this role to Alpha for efficiency. Alternatively, a dedicated address or Atomist can manage it.

#### Best Practices

- Ensure markets selected for instant withdrawals are highly liquid to avoid liquidation risks.
- Use timelocks for changes to fuse configurations to allow review by Guardians or LPs.

### WITHDRAW_MANAGER_REQUEST_FEE_ROLE (901)

This role sets the request fee, charged when an LP submits a withdrawal request from a scheduled withdrawal vault. The fee is applied at the time of the request, not redemption. For strategies with algorithmically adjusting fees, assign this role to Alpha. Alternatively, Atomists can manage it based on the vault’s strategy.

#### Best Practices

- Use a multisig wallet for managing this role to enhance security.
- Monitor fee adjustments to align with the vault’s strategy and LP expectations.

### WITHDRAW_MANAGER_WITHDRAW_FEE_ROLE (902)

This role sets the withdrawal fee, analogous to the request fee but charged during instant withdrawals.

#### Best Practices

- Align withdrawal fees with the vault’s operational strategy.
- Use a multisig wallet for secure management.

### UPDATE_MARKETS_BALANCES_ROLE (1000)

This role manually updates cached market balances for deposits or withdrawals if automation (e.g., via pre-hooks) is not enabled. This is necessary for less active vaults to ensure accurate balance reporting. For efficiency, assign this role to an automated service.

#### Best Practices

- Automate balance updates with pre-hooks when possible to reduce manual intervention.
- Grant this role to a secure, automated service to ensure timely updates.

### UPDATE_REWARDS_BALANCE_ROLE (1100)

Similar to the previous role, this role updates the balances of incentives claimable by the vault through the rewards manager. For automated workflows, assign this role to Alpha.

#### Best Practices

- Integrate with automated reward tracking systems to streamline updates.
- Use a secure wallet or service for this role to prevent unauthorized access.

### CLAIM_REWARDS_ROLE (600)

This role allows claiming rewards on behalf of the vault. For practical reasons, assign this role to Alpha to automate reward collection.

#### Best Practices

- Use a secure, automated system like Alpha to ensure timely reward claims.
- Monitor reward claims to verify correct distribution.

### TRANSFER_REWARDS_ROLE (700)

This role enables the transfer of claimed rewards to the rewards claim manager. In the IPOR Fusion Plasma Vault, reward management (e.g., selling, staking, or transferring) can be handled by a dedicated module. If no such module exists, Alpha can manage rewards manually. Assigning this role to Alpha allows it to transfer rewards to itself for processing.

#### Best Practices

- Use a dedicated rewards management module when available to streamline operations.

## Guardian Role

This role serves as the emergency brake for the vault. The Guardian has the authority to reject time-locked actions and to [pause](https://ipor.io/docs/build-on-fusion/developer-guide/pause-functionality.md) or unpause the vault in case of an emergency.

- Configurable by the Owner
- Allows for multiple users
- **Cannot be timelocked:** Guardians must always retain a 0-day execution delay to function as an instant emergency brake.

**Guardian Capabilities & Limitations:**

- **Veto Configuration Changes:** The Guardian can unilaterally cancel pending operational and configuration transactions scheduled by the Owner, Atomist, or Fuse Manager (any function gated through the access manager's target-function-role map).
- **Emergency Pause:** The Guardian can instantly pause the vault, freezing all execution actions (including user deposits and withdrawals).
- **LIMITATION - No Role Management Veto:** Crucially, role-management calls (`grantRole`, `revokeRole`, `setGrantDelay`) are OpenZeppelin admin functions. **Guardians CANNOT cancel these actions.** Only the scheduler themselves or an `ADMIN` can cancel role changes. For permission changes, the timelock provides a _detection-and-exit window_ for depositors, not a Guardian veto.

#### **Best Practices**

- **Instant Reaction:** Guardians must hold a **0-day execution delay** so they can react instantly to emergencies.
- **Key Separation:** Guardian keys must be independent and separate from Owner keys. The emergency brake must fail independently of the roles it is meant to brake.
- **Monitoring is Mandatory:** Because Guardians cannot veto role changes, robust on-chain monitoring for `RoleGranted` and `RoleRevoked` events is required to alert depositors of unauthorized permission changes during the exit window.
- If using automated risk monitoring, consider granting the Guardian role to an emergency mechanism to enable automatic vault pausing.

## Fuse Manager Role

The account with this role has rights to manage the FuseManager contract, add or remove fuses, balance fuses and reward fuses.

- Configurable by the Atomist
- Allows for multiple users

#### Best Practices

- For a more decentralized setup, use a dedicated, time-locked account to manage fuses, as they impact the strategy.

## Claim Rewards Role

Account with this role has rights to claim rewards from the PlasmaVault using and interacting with the RewardsClaimManager contract.If PlasmaVault has a dedicated smart contract to handle the harvested rewards, the role should be granted to that contract.

- Configurable by the Atomist
- Allows for multiple users

## Transfer Rewards Role

An account with this role has rights to transfer rewards from the PlasmaVault to the RewardsClaimManager.If PlasmaVault has a dedicated smart contract to handle the harvested rewards, the role could be granted to that contract.

- Configurable by the Atomist
- Allows for multiple users

## Rewards Claim Manager Role

Technical role for the RewardsClaimManager contract. Account with this role has rights to claim rewards from the PlasmaVault. For practical reasons, it can be granted to the same entity holding either an Alpha or Atomist role.Configurable by the AtomistAllows for multiple usersPerformance Fee Manager RoleThe account with this role has rights to manage the performance fee, define the performance fee rate, and manage the performance fee recipient.Self managedAllows for multiple usersManagement Fee Manager RoleThe account with this role has rights to manage the management fee, define the management fee rate, and manage the management fee recipient.Self-managedAllows for multiple usersWhitelist RoleThe account with this role has rights to deposit/mint and withdraw/redeem assets from the Plasma Vault.Configurable by the AtomistAllows for multiple usersConfig Instant Withdrawal Fuses RoleAn account with this role has rights to configure instant withdrawal fuses order.

## Technical Roles (Must remain at 0-day delay)

Certain roles act as the internal plumbing of the IPOR Fusion system. These roles are held by system contracts (or automated keepers) and are essential for the fundamental operation of the vault.

Applying an execution delay (timelock) to any of these roles will break the vault's core mechanics. The following roles **must always remain at a 0-day execution delay** and should never have a minimal execution delay (floor) set:

- `TECH_PLASMA_VAULT` (3)
- `TECH_CONTEXT_MANAGER` (5)
- `TECH_WITHDRAW_MANAGER` (6)
- `TECH_VAULT_TRANSFER_SHARES` (7)
- `TECH_REWARDS_CLAIM` (601)
- `TECH_PERF_FEE` (400) and `TECH_MGMT_FEE` (500) _(Only when held by the internal FeeManager/plumbing on factory vaults. If held by governance, these require a 3-day delay)._

## **PreHooksManager** Role

As outlined in the [pre-hook configuration](https://ipor.io/docs/build-on-fusion/developer-guide/configuring-pre-hooks.md), all changes made by the PreHooksManager must be subject to a timelock. This delay allows liquidity providers (LPs) and other administrators to respond if pre-hooks are set incorrectly.

#### Best practices

- Implement a sufficiently long timelock for PreHooksManager actions.
- Use a multisig wallet to administer pre-hooks for enhanced security.

## **Whitelist / Open Vault**

By default, each new vault in the IPOR Fusion system is initialized with a whitelist, restricting minting and redeeming of shares to whitelisted addresses only. Atomists can add addresses to the whitelist or disable it entirely, making the vault open to all users. Once the whitelist is disabled, it cannot be re-enabled.

#### Best Practices

- Retain the whitelist during the testing period, even if you plan to open the vault to all users.
- Use a secure multisig wallet for Atomists managing the whitelist to enhance security.

## Cross-Chain Access Management

For Fusion vaults that utilize cross-chain strategies, the access management model is extended to secure the asynchronous nature of remote capital deployment. This includes dedicated attestation and emergency recovery roles:

- **Attestation Roles:** Independent Proposer and Approver roles are required to validate and recognize remote Net Asset Value (NAV) on the source chain.
- **Emergency Roles:** Rescue Admin and Rescue Guardian roles provide a bifurcated, time-delayed break-glass mechanism for severe bridge or transport failures.

To maintain maximum security, the protocol strictly enforces that these paired roles (Proposer/Approver and Admin/Guardian) must be held by separate, independent addresses.

For a complete breakdown of these specialized roles and their operational responsibilities, please refer to the [**Governance and Operating Roles**](https://ipor.io/docs/build-on-fusion/atomists/curating-a-fusion-vault/cross-chain-vault-integration.md) section in the Cross-Chain Vault Integration: Business & Operations Guide.


---

Source: https://ipor.io/docs/build-on-fusion/developer-guide/mcp.md

# MCP

> Connect to the hosted Fusion MCP server: tools, resources, prompts and a worked example for inspecting vaults.

## MCP

Model Context Protocol server for IPOR Fusion. Use it to inspect deployed vaults, resolve addresses and research markets from any MCP-capable client (Claude, Cursor, custom agents). This server is **read-only inspection and research** — it does not create or configure vaults. For that, use the Python SDK (see also Deploy your first Fusion vault).

### Connect

**Transport:** Streamable HTTP **Server URL:** `https://mcp.ipor.io/mcp`

Add it through the MCP settings of your client — do not open the URL as a normal webpage.

Quick add (Claude Code):

```
claude mcp add --transport http ipor-fusion https://mcp.ipor.io/mcp
```

Manual `mcpServers` config (Claude Desktop, Cursor, and similar clients):

```json
{
  "mcpServers": {
    "ipor-fusion": {
      "url": "https://mcp.ipor.io/mcp"
    }
  }
}
```

For a locally installed alternative, the same server ships bundled with the Python SDK: [Install the SDK-bundled Fusion MCP server](https://github.com/IPOR-Labs/ipor-fusion.py#mcp-server).

### Tools

| Tool                                                                    | What it does                                                                                                                                                                     |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vault_info(vault_address, chain_id, block_number=0)`                   | Full on-chain state of a Plasma Vault: asset, TVL, supply cap, fees, managers, fuses, balance fuses, substrates, alpha config, health check.                                     |
| `vault_oracle_mapping(vault_address, chain_id, block_number=0)`         | Resolves how a vault prices every configured asset, recursing through composed and derived price feeds (Chainlink, ERC4626 share price, Morpho collateral, middleware fallback). |
| `vaults_list()`                                                         | All IPOR Fusion vaults from the public API: chain, address, name, asset, APY, TVL.                                                                                               |
| `fusion_addresses_list(chain_id)`                                       | The full `{contract name: address}` map for one chain, sourced from `ipor-abi`.                                                                                                  |
| `fusion_address_names(chain_id=0)`                                      | Discover which contract names exist, across all chains or for one chain.                                                                                                         |
| `fusion_address_lookup(query, chain_id=0)`                              | Resolve a contract by name (substring) or by address, across all Fusion deployments.                                                                                             |
| `market_morpho_blue(market_id, chain_id, block_number=0, no_api=false)` | Inspect a Morpho Blue market: params, on-chain state, borrow/supply APY, supplying MetaMorpho vaults.                                                                            |
| `market_meta_morpho(vault_address, chain_id)`                           | Inspect a MetaMorpho V1 or Morpho Vault V2: roles, fees, allocations/adapters, per-market caps and headroom.                                                                     |

### Resources (`fusion://`)

| Resource                | Contents                                                                                                     |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| `fusion://glossary`     | Canonical terms — Atomist, Alpha, Fuse, Plasma Vault, and the rest of the vocabulary used across these docs. |
| `fusion://architecture` | How the pieces of a Fusion vault fit together.                                                               |
| `fusion://invariants`   | Common revert selectors and the role table an agent should know before sending a transaction.                |
| `fusion://quickstart`   | The executed vault-deploy walk (also mirrored in the SDK and the Deploy your first Fusion vault page).       |

### Prompts

| Prompt                 | Use it to                                             |
| ---------------------- | ----------------------------------------------------- |
| `quickstart`           | Orient a fresh agent to Fusion.                       |
| `deploy_vault`         | Walk through creating and configuring a vault.        |
| `analyze_vault`        | Inspect an existing vault's configuration and health. |
| `trace_oracle_pricing` | Follow a vault's price-feed chain end to end.         |
| `explain_fuse`         | Explain what a specific Fuse does.                    |

### Worked example: inspect a vault

```python
# 1. Get the vault's full on-chain state
vault_info(
    vault_address="0x...",
    chain_id=8453,  # Base
)
# -> asset, total_assets, total_supply, fees, fuses, balance_fuses, substrates, alpha config

# 2. Understand how it prices its assets
vault_oracle_mapping(
    vault_address="0x...",
    chain_id=8453,
)
# -> per-asset price source, feed type, and the resolved price for each configured asset
```

Combine the two to answer "what is this vault holding, and can I trust its price feeds?" without reading a single line of source.

### Rules for agents

- This server is for deployed-state inspection and research only.
- Use the Python SDK for vault creation and transaction construction.
- Resolve production addresses through `fusion_address_lookup` / `fusion_addresses_list`, not by guessing or copying from tests.
- An address returned here does not, by itself, prove a Fuse or component is compatible with a particular vault, market or strategy.

