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
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.
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:
cloneversusclone_supervised: same arguments; the supervised variant is gated by a maintenance role. Useclone.- The balance fuse above is the
BalanceFuseAaveV3entry 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, readPlasmaVault(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.substratesand documented in the fuse libraries of the contracts repository (opens in new tab). - 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 (opens in new tab). - The four addresses come from the
ipor-abiregistry:fetch_address_registry(ChainId(8453))returns them by name (.address("IporFusionFactoryProxy"),.address("SupplyFuseAaveV3"),.address("BalanceFuseAaveV3")), as doesfusion 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_ROLEto the bot's address instead ofowner, 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:
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(opens in new tab), read from the SDK withfetch_address_registry(chain_id), the CLI'sfusion address lookup, or the hosted MCP server'sfusion_address_lookup - The SDK runs this exact walk in one
eth_simulateV1batch:tests/test_simulate_vault_from_scratch_base.pyinipor-fusion.py(opens in new tab) - A strategy bot that operates an existing vault:
ipor-fusion-alpha-example(opens in new tab)