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 (opens in new tab), source (opens in new tab)). 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.

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 (opens in new tab).

Install

pip install ipor-fusion    # Python 3.11+

Connect and execute

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 (opens in new tab) (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 Calls 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:

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:

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.

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:

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.

# 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.
{
  "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 (opens in new tab) 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:

# 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/ (opens in new tab). They preview and simulate through eth_simulateV1 — nothing is ever signed or broadcast.

For a strategy bot that operates an existing vault end to end, see ipor-fusion-alpha-example (opens in new tab).

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 (opens in new tab). Submodules of a public subpackage, such as ipor_fusion.core.contract, are internal.

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 (opens in new tab)) 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)

ModulePurpose
Web3ContextProvider connection, signing, tx dispatch, gas estimation
CallPre-encoded contract call; .call(ctx), .send(ctx), .calldata, .build_transaction(ctx)
Multicall3Batch independent reads into one eth_call (batch(), aggregate, try_aggregate)
PlasmaVaultERC-4626 vault — execute, deposit, withdraw, fuse and market configuration
VaultSimulatorBatch execute + reads through eth_simulateV1, multi-block, no local node
FusionFactoryDeploy a new vault (clone, clone_supervised)
AccessManagerRoles (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
RewardsManagerClaim and vest rewards
WithdrawManagerTime-windowed withdrawal requests
FeeManagerDeposit, performance, and management fee configuration
PriceOracleMiddleware / PriceOracleMiddlewareManagerAsset price feeds and per-vault overrides
ERC20Token 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 (opens in new tab).

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:

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 (opens in new tab) in the repository.

Last updated