What a Market Integration Contains
The components of a new market integration: action fuses, a balance fuse, claim fuses, a substrate library, price feeds and a market id, with the reference families to copy from.
A market integration is a family of contracts that you write in your own repository and deploy yourself. This page lists the components, the interface each one implements, and what must be checkable on-chain. For the process around them, start with the overview. For the coding rules, see the coding standard and security requirements. For how a single fuse is built, see Developing a Fuse.
Two families in the Fusion repository show the pattern, as read at commit 632350d: contracts/fuses/erc4626 (opens in new tab) is the simple one (a supply fuse and a balance fuse), and contracts/fuses/aave_v4 (opens in new tab) is a full lending market (supply, borrow and collateral fuses, a balance fuse, a substrate library, a README and tests).
Components at a glance
| Component | Interface | Required | Reference |
|---|---|---|---|
| Action fuses (enter, exit) | IFuseCommon | Yes | Erc4626SupplyFuse, AaveV4SupplyFuse |
| Instant withdraw | IFuseInstantWithdraw | Optional | Erc4626SupplyFuse, AaveV4SupplyFuse |
| Transient variants | enterTransient(), exitTransient() | Optional | Erc4626SupplyFuse |
| Balance fuse | IMarketBalanceFuse | Yes, or ZeroBalanceFuse | Erc4626BalanceFuse, AaveV4BalanceFuse |
| Claim fuse | none specific | Optional | MorphoClaimFuse |
| Substrate library | none specific | When a substrate carries more than an address | AaveV4SubstrateLib |
| Price feed | IPriceFeed | When the middleware has no price source for an asset | none specific |
| Market id | constant in IporFusionMarkets | Yes | none specific |
| README | none | Yes, covering the market, the substrates and the venue risks | aave_v4/README.md |
Action fuses
An action fuse moves the vault into or out of a position. An action fuse is typically named <Venue><Action>Fuse, such as AaveV4SupplyFuse, with one contract per file and the file named after the contract.
The fuse implements IFuseCommon (opens in new tab), which has one function, MARKET_ID(). The vault engine itself requires only that function: it reads MARKET_ID() from the fuse and delegatecalls the encoded call, so the action functions are reached by the Alpha (the role that executes strategy transactions on the vault) through the call data of a FuseAction. A vault runs only fuses that were added to it; any other address reverts with UnsupportedFuse.
The reference families do not implement IFuse, which declares enter(bytes) and exit(bytes). They expose typed functions instead, for example enter(Erc4626SupplyFuseEnterData) and exit(AaveV4SupplyFuseExitData).
The ERC4626 example is the shortest one to read, but it is not a model for every requirement. It does not reject market id 0 in the constructor (the guard is shown on AaveV4SupplyFuse), and it does not set the allowance back to zero after approving (shown on AguaSupplyFuse). Its enter, exit and transient functions have no NatSpec; the Aave V4 fuses show the expected level.
Instant withdraw
Implementing IFuseInstantWithdraw (opens in new tab) is optional. An action fuse that implements it exposes instantWithdraw(bytes32[] params), where params[0] is the amount in the underlying asset and the following params are specific to the fuse. The layouts differ per family: ERC4626 uses params[1] as the vault address; Aave V4 uses params[1] to params[4] for asset, spoke, reserve id and a reserved slot.
During a user withdrawal the vault sets params[0] to the amount still needed and delegatecalls instantWithdraw on each configured fuse in order, until the amount still needed is covered. The call is not wrapped in try/catch, so a revert inside it reverts the user withdrawal. This is why the reference fuses catch only the venue withdraw call on this path and report a failure with an event. In the ERC4626 example the catch covers only vault.withdraw: the substrate check and the maxWithdraw read are not caught, and instantWithdraw passes maxSharesBurned = 0, which switches the burned-shares limit off on that path. A zero requested or clamped amount returns 0 without an event.
Transient variants
enterTransient() and exitTransient() read their inputs from TransientStorageLib, keyed by the fuse VERSION, call the same logic, and write the outputs back to transient storage. Both reference families provide them.
Balance fuse
Every market needs a balance fuse, because the vault engine asks it for the market balance. It implements IMarketBalanceFuse (opens in new tab): balanceOf() returns the balance of the vault in the market in USD with 18 decimals (WAD). Use this exact signature. The interface does not declare balanceOf() as view; the reference implementations declare it view. You may declare it view, as the reference implementations do.
The vault calls it by delegatecall in the context of the vault and does not catch a revert, so a reverting balance fuse blocks the balance update. The fuse reads its substrates with PlasmaVaultConfigLib.getMarketSubstrates(MARKET_ID), prices each asset through the Price Oracle Middleware and converts the result to WAD. An atomist registers it with addBalanceFuse(marketId, fuse), which reverts with BalanceFuseMarketIdMismatch if the fuse MARKET_ID() differs from the market id.
ZeroBalanceFuse
ZeroBalanceFuse (opens in new tab) always returns 0 and takes the market id in its constructor. Use it only for a market whose fuse holds no assets and creates no separate position of its own, with the position valued on another market through a dependency graph. The LitePSM market works this way: it holds nothing itself, the sUSDS position is valued on a separate market, and a dependency graph links the two.
Never use it for a position you cannot value, because it hides the position from the vault balance. You deploy it from the unchanged Fusion source with your own market id. It has MARKET_ID but no VERSION and does not reject market id 0; those requirements apply to the fuses you write.
Prices
The balance fuse prices assets through the Price Oracle Middleware, which returns USD prices with 18 decimals. For an asset with no registered source the middleware falls back to a Chainlink feed registry if one is configured, and otherwise reverts; it also reverts for a price that is zero or negative. A vault created by the Fusion factory reads prices through its own Price Oracle Middleware Manager: the vault's price-manager role sets a source per asset there with setAssetsPriceSources(assets, sources), and an asset without a vault-specific source falls back to the shared middleware, whose owner registers sources with setAssetsPricesSources(assets, sources). A balance fuse that prices an asset neither of them can price reverts.
If an asset needs a custom source, implement IPriceFeed (opens in new tab) (decimals() and latestRoundData()). The middleware reads the feed's decimals and price, so report decimals correctly. A third-party feed must not be manipulable within one transaction.
Substrates
A substrate is a bytes32 value that an atomist grants to a market; fuses check substrates before moving funds. Plain address substrates are checked with isSubstrateAsAssetGranted, other encodings with isMarketSubstrateGranted.
When a substrate carries more than an address, put the encoding and decoding in a substrate library, as AaveV4SubstrateLib (opens in new tab) does: a type flag in the top byte, then the fields, with reserved bits that must be zero. It looks permissions up in constant time and rejects non-canonical words, so a hand-encoded word that does not match the canonical encoding never grants anything. Aave V4 uses one market id for all Spokes; the substrates select the Spoke and the reserve.
Claim fuses
If the venue pays out rewards, add a claim fuse. In the Fusion repository, claim fuses live in contracts/rewards_fuses/<venue>/; in your repository the location is your choice. A claim fuse is registered in the vault's RewardsClaimManager with addRewardFuses (fuse manager role), not with the vault's addFuses. The claim role calls RewardsClaimManager.claimRewards, which reverts with FuseUnsupported for a fuse that is not registered there and then calls PlasmaVault.claimRewards, which delegatecalls the fuse; the fuse forwards the claimed rewards to the RewardsClaimManager. As MorphoClaimFuse (opens in new tab) does, it checks that the distributor is granted as a substrate before claiming and exposes VERSION and MARKET_ID.
Market id
Third-party markets use ids from 400_001 to 400_999. Use the next free number in that range; the IPOR team confirms it in the pull request. The id is added as one constant in IporFusionMarkets (opens in new tab), the library where Fusion markets are listed and described. Every fuse must expose MARKET_ID(), because when a fuse is added the Fuse Whitelist Registry reads MARKET_ID() inside try/catch, and a fuse without it is silently skipped for the per-market list. MARKET_ID is immutable, so a fuse deployed with an id that changes later must be redeployed.
Dependency balance graphs
When one action changes the balance of two markets, for example the vault's own token holdings (the ERC20_VAULT_BALANCE market) and your market, the markets need a dependency balance graph. The atomist sets it with updateDependencyBalanceGraphs(marketIds, dependencies). A call overwrites the dependencies of each listed market, and dependencies are directional. When a market balance is refreshed, the markets it depends on are refreshed in the same update. Describe the graph your market needs in the pull request.
What must be verifiable on-chain
The IPOR team reviews the code that is deployed, so the following must be checkable from the chain:
- The source of every fuse is verified in the block explorer of its chain, and the verified source carries the exact compiler settings of the deployment (see the coding standard).
VERSION()returns the fuse's own address,MARKET_ID()equals the constant from the pull request, and the constructor arguments are the ones listed in the pull request.- The fuse has no storage variables other than ERC-7201 namespaced slots, and no roles, owner, admin keys, pause or upgrade mechanism; it is not a proxy.
- On every chain you use the same source code and the same compiler settings. The runtime bytecode can still differ, because immutable values (
VERSION,MARKET_ID, venue addresses) are written into it. List these expected differences, and any other difference such as a different compiler or EVM version, in the pull request.
Deployed fuses cannot be changed: a fix or a new version means a new deployment with new addresses. The market id stays the same, so no new pull request is needed; send the new addresses and their verified sources to the IPOR team in a ticket in the IPOR Discord (opens in new tab) for review. Vaults already using the old address must migrate.
README in your repository
Keep a README next to the fuses. It describes the market, the substrates and the venue risks (see Security Requirements). A good one follows the sections of the Aave V4 README (opens in new tab): overview, market structure, key components, substrate configuration with the validation matrix, operations, balance calculation, security considerations, price oracle setup and tests. Link it from the pull request; see Whitelist Review.