Coding Standard
What IPOR requires from the code of a third-party fuse family: structure, statelessness, safety checks, errors, events, NatSpec, compiler settings and formatting, plus recommended tests.
This page lists what the IPOR team requires when it reviews the deployed code of your fuses. In this section "IPOR requires" and "IPOR expects" both mean a rule that the review applies: a fuse that does not meet it receives a request for changes in the pull request. "Recommended" marks what is optional. Each rule is marked as an IPOR requirement or shown in the code of the reference fuses in IPOR-Labs/ipor-fusion (opens in new tab), read at commit 632350d. Where a reference fuse does not follow a rule, the page says so. For a walkthrough of a working fuse, see Developing a Fuse. For the safety rules behind several of the items below, see Security Requirements.
The review is based on what is on-chain: the source code verified in the block explorer of each chain. Write the code so that it can be read and checked in that form.
Naming and file layout
An action fuse written for a new market is typically named <Venue><Action>Fuse, for example AaveV4SupplyFuse, AaveV4BorrowFuse and AaveV4CollateralFuse. Use one contract per file and name the file after the contract. The reference families follow this layout: see AaveV4SupplyFuse (opens in new tab) and Erc4626SupplyFuse (opens in new tab).
Every fuse exposes MARKET_ID(). The IFuseCommon interface has exactly this one function (source (opens in new tab)). When a fuse is added to the Fuse Whitelist Registry, the registry reads MARKET_ID() and lists the fuse under that market id; a fuse without MARKET_ID() is silently skipped for the per-market list, so IPOR requires it (source (opens in new tab)). The parts of a market integration are described in What a Market Integration Contains.
Statelessness and delegatecall
A fuse runs by delegatecall in the vault context. Inside a fuse function address(this) is the vault, and reads of storage see the vault's storage (PlasmaVault.execute delegatecalls the encoded call). For this reason IPOR requires fuses to be stateless:
- The only variables are immutables:
VERSION,MARKET_IDand, where the venue is fixed, venue addresses. There are no mutable storage variables. - Any other state lives in ERC-7201 namespaced storage slots, accessed through a library. Fusion uses this pattern for the
AsyncExecutoraddress and for the transient storage slot. VERSIONis set toaddress(this)in the constructor, so it is the address of the fuse contract itself and not of the vault. The fuse emits it in events and uses it as the transient storage key.
The reference declaration and constructor of the ERC4626 fuse are in lines 81-87 (opens in new tab).
Reject market id zero
IPOR requires the constructor to revert when marketId_ == 0. The vault engine uses market id 0 as the empty slot when it lists and refreshes markets, so a fuse with market id 0 would be skipped. AaveV4SupplyFuse shows the guard with the custom error AaveV4SupplyFuseInvalidMarketId, and a unit test asserts the revert (constructor (opens in new tab)).
Erc4626SupplyFuse and Erc4626BalanceFuse do not have this guard at 632350d. Do not copy their constructors as they are; add the check yourself.
Check substrates before moving funds
A substrate is a bytes32 value that an atomist grants to a market. The fuse checks it before it moves any funds: the grant check comes before every approve, deposit, supply or withdraw. Plain address substrates are checked with isSubstrateAsAssetGranted; other encodings with isMarketSubstrateGranted. Both are in PlasmaVaultConfigLib.
When a caller supplies an asset together with a venue position, also check that the asset matches the asset of the granted position before moving funds. AaveV4SupplyFuse compares the reserve underlying with the asset (lines 145-148 (opens in new tab)). The ERC4626 fuse checks the grant at the start of enter (lines 94-96 (opens in new tab)).
If a substrate carries more than an address, put its encoding and decoding in a substrate library, as AaveV4SubstrateLib does: a type flag in the top byte, then the fields, with reserved bits that must be zero.
Token transfers and approvals
- Use
SafeERC20and approve the exact amount withforceApprove. - IPOR requires the allowance to be set back to zero after the venue call, so that no residual allowance remains.
AguaSupplyFuseshows the patternforceApprove(vault, amount), deposit,forceApprove(vault, 0)(lines 83-87 (opens in new tab)).
The allowance reset is a requirement of the standard, not a feature of the ERC4626 and Aave V4 examples. Erc4626SupplyFuse.enter, AaveV4SupplyFuse.enter and AaveV4BorrowFuse.exit approve the exact amount but do not reset it at 632350d.
Errors and events
- Errors are custom errors named with the fuse name as a prefix, for example
AaveV4SupplyFuseInvalidMarketId. Do not use revert strings. - Events are named with the fuse name as a prefix and carry the fuse
VERSIONas the first parameter. - Events report what actually happened. Emit the amount after clamping to the balance or limit, not only what was requested. The requirement is that events show the outcome; it is not that every event has an amount field.
AaveV4SupplyFuseEnter, for example, reports the received shares. - No
indexedparameters. IPOR requires that event parameters are not indexed; all parameters go into the data section. The reference fuses declare their events withoutindexed. Some other Fusion contracts, for exampleFusionFactory, still use it, so this is a rule for new fuses and not a description of all existing code.
NatSpec
Write NatSpec in English: @title, @notice, @param, @return and @custom:revert on external and public functions, and @param on events and errors. The Aave V4 fuses show this level of documentation. The enter, exit, enterTransient and exitTransient functions of Erc4626SupplyFuse have no NatSpec at 632350d.
A fuse README in your repository should follow 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.
Compiler settings and size
- Pragma. IPOR sets no requirement on the pragma version of your code; you choose it. Fusion interfaces and libraries such as
IFuseCommonorPlasmaVaultConfigLibdeclarepragma solidity 0.8.30, so a file that imports them must be compiled with solc 0.8.30. - Reproducible bytecode. The deployed code is read from the chain, so the compiler settings you used for the deployment must reproduce the deployed bytecode. List them in the pull request: the solc version, the EVM version, the optimizer setting and runs, and any per-chain profile.
- Same source on every chain. Use the same source code and the same compiler settings on every chain. 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, in the pull request. - No
via_ir. IPOR does not acceptvia_irwhen compiling fuses. At commit632350d, Fusion's ownfoundry.toml(opens in new tab) sets solc 0.8.30,optimizer_runsof 10000000 andevm_versioncancun, with anarbitrumprofile on paris, and novia_ir. - EIP-170. The deployed runtime bytecode of every fuse must fit the limit of 24,576 bytes. If the code is too large, reduce it in the code, for example by splitting logic into libraries, and not with compiler flags.
Formatting and linting
Fusion formats Solidity with Prettier and prettier-plugin-solidity (.prettierrc.js (opens in new tab): tab width 4, print width 120, double quotes, no bracket spacing, the slang parser). It lints with solhint, extending solhint:recommended (.solhint.json (opens in new tab)). Following both is a recommendation. Code that follows these files is easier to read.
Recommended: tests
Recommended, not required.
Tests are not a condition of the review. They are the quickest way to show that your fuses behave as this page describes. Fusion's own tests use Foundry (forge test). Good tests prove the following:
- Every branch of every fuse function runs, including the early returns for zero amounts.
- Every custom error is asserted by its selector with
vm.expectRevert, for example the market id zero guard in the constructor. - Every event is asserted with
vm.expectEmit, including the amounts after clamping. - Fork tests against the real venue. Run them on a pinned block so results are reproducible. The Aave V4 fork tests pin an Ethereum mainnet block with
vm.createSelectFork(AaveV4ForkTestBase.sol(opens in new tab)). - The balance fuse against the vault. After
enterandexit, the balance fuse and the vault'stotalAssets()agree. - Edge cases. Instant withdraw, slippage limits, zero and dust amounts, tokens with different decimals, and a substrate that was not granted.
A public repository with the tests and the fork-test results is recommended, and the pull request description may link to it. See Whitelist Review for what the pull request contains.