# Developing a Fuse

> Anatomy of an action fuse and its balance fuse, shown on the ERC4626 supply fuse: data structs, enter, exit, transient variants, instant withdraw, events and errors.

> **Note:** If you are integrating a new market, start with [Integrating a New Market](https://ipor.io/docs/build-on-fusion/integrating-a-new-market.md). It describes the whole process and the rules for the fuses you write. This page explains how a single fuse is built.

A fuse is a small stateless contract that the vault runs by `delegatecall`. This page walks through the generic ERC4626 fuses of the public [`IPOR-Labs/ipor-fusion`](https://github.com/IPOR-Labs/ipor-fusion) repository, read at commit `632350d`: [`Erc4626SupplyFuse`](https://github.com/IPOR-Labs/ipor-fusion/blob/main/contracts/fuses/erc4626/Erc4626SupplyFuse.sol) (an action fuse) and [`Erc4626BalanceFuse`](https://github.com/IPOR-Labs/ipor-fusion/blob/main/contracts/fuses/erc4626/Erc4626BalanceFuse.sol) (its balance fuse). Excerpts are short and link to the exact lines. Where the example differs from what the [Coding Standard](https://ipor.io/docs/build-on-fusion/integrating-a-new-market/coding-standard.md) asks of new fuses, this page says so.

## The shape of an action fuse

An action fuse moves the vault into or out of a position on an external venue. The [Alpha](https://ipor.io/docs/build-on-fusion/architecture-overview/what-is-an-alpha.md) (the role that executes strategy transactions on the vault) runs it through `PlasmaVault.execute`. The vault runs the call only for a fuse address that was added to that vault with `addFuses(address[])`, reads `MARKET_ID()` from the fuse and then delegates the encoded call. Because of the `delegatecall`, `address(this)` inside the fuse is the vault, and storage reads see the vault's storage.

```solidity
contract Erc4626SupplyFuse is IFuseCommon, IFuseInstantWithdraw {
```

Source: [`Erc4626SupplyFuse.sol#L37`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L37)

`IFuseCommon` has one function, `MARKET_ID()`. `IFuseInstantWithdraw` is optional. The reference fuses do not implement `IFuse` (`enter(bytes)` and `exit(bytes)`); they expose typed functions, such as `enter(Erc4626SupplyFuseEnterData)`.

The contract holds two immutables, and nothing else is stored:

```solidity
    address public immutable VERSION;
    uint256 public immutable MARKET_ID;

    constructor(uint256 marketId_) {
        VERSION = address(this);
        MARKET_ID = marketId_;
    }
```

Source: [`Erc4626SupplyFuse.sol#L81-L87`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L81-L87)

- `VERSION` is the address of the fuse contract itself, not of the vault. It is the first parameter of every event and the key of the transient storage.
- `MARKET_ID` identifies the market the fuse belongs to. It is immutable, so a fuse deployed with a wrong id must be redeployed.

This constructor does not reject `marketId_ == 0`. IPOR requires new fuses to do so, because the vault engine uses market id `0` as an empty slot. The guard is shown on `AaveV4SupplyFuse`:

```solidity
    /// @notice Constructor for AaveV4SupplyFuse
    /// @param marketId_ The Market ID associated with the Fuse
    constructor(uint256 marketId_) {
        if (marketId_ == 0) {
            revert AaveV4SupplyFuseInvalidMarketId();
        }
        VERSION = address(this);
        MARKET_ID = marketId_;
    }
```

Source: [`AaveV4SupplyFuse.sol#L123-L131`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/aave_v4/AaveV4SupplyFuse.sol#L123-L131)

## Data structs

`enter` and `exit` take a struct, so the Alpha encodes the call data once and the fuse decodes it without manual parsing.

```solidity
/// @notice Data structure for entering - supply - the ERC4626 vault
struct Erc4626SupplyFuseEnterData {
    /// @dev ERC4626 vault address
    address vault;
    /// @dev amount to supply, this is amount of underlying asset in the given ERC4626 vault
    uint256 vaultAssetAmount;
    /// @dev minimum amount of shares expected to receive from the deposit
    uint256 minSharesOut;
}
```

Source: [`Erc4626SupplyFuse.sol#L15-L23`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L15-L23)

```solidity
/// @notice Data structure for exiting - withdrawing - the ERC4626 vault
struct Erc4626SupplyFuseExitData {
    /// @dev ERC4626 vault address
    address vault;
    /// @dev amount to withdraw, this is amount of underlying asset in the given ERC4626 vault
    uint256 vaultAssetAmount;
    /// @dev maximum number of vault shares that can be burned during the withdraw, 0 means no limit
    uint256 maxSharesBurned;
}
```

Source: [`Erc4626SupplyFuse.sol#L25-L33`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L25-L33)

Both structs carry a caller-supplied limit: `minSharesOut` on enter and `maxSharesBurned` on exit, where `0` means no limit.

## enter

`enter` returns early, without an event, when the requested amount is `0`. Next it checks that the vault is a granted substrate of the market:

```solidity
        if (!PlasmaVaultConfigLib.isSubstrateAsAssetGranted(MARKET_ID, data_.vault)) {
            revert Erc4626SupplyFuseUnsupportedVault("enter", data_.vault);
        }
```

Source: [`Erc4626SupplyFuse.sol#L94-L96`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L94-L96)

The substrates of this fuse are the addresses of the ERC4626 vaults the atomist allowed. The check comes before any approval or deposit. Then `enter` limits the amount to the asset balance of the vault, again returning `0` without an event if nothing is left, and moves the funds:

```solidity
        ERC20(underlyingAsset).forceApprove(data_.vault, finalVaultAssetAmount);

        uint256 shares = IERC4626(data_.vault).deposit(finalVaultAssetAmount, address(this));

        if (shares < data_.minSharesOut) {
            revert Erc4626SupplyFuseInsufficientShares(shares, data_.minSharesOut);
        }

        emit Erc4626SupplyFuseEnter(VERSION, underlyingAsset, data_.vault, finalVaultAssetAmount);
```

Source: [`Erc4626SupplyFuse.sol#L109-L117`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L109-L117)

In this excerpt, `underlyingAsset` is the asset of the ERC4626 vault and `finalVaultAssetAmount` is the requested amount limited to the balance of the vault.

The fuse approves exactly the amount it deposits, deposits to `address(this)` (the vault), reverts with `Erc4626SupplyFuseInsufficientShares` when fewer shares than `minSharesOut` are received and emits the event with the amount that was actually supplied. This example does not set the allowance back to zero after the deposit. IPOR requires new fuses to do that; the pattern is shown on `AguaSupplyFuse` (`forceApprove(vault, amount)`, deposit, `forceApprove(vault, 0)`).

## exit

`exit` and `instantWithdraw` share one internal function, `_exit`. It does the following:

- returns `0` without an event when the requested amount is `0`;
- reverts with `Erc4626SupplyFuseUnsupportedVault` when the vault is not a granted substrate, on the instant-withdraw path too;
- limits the amount to `maxWithdraw(address(this))` of the vault, and returns `0` without an event when that is `0`.

A zero `maxWithdraw` is therefore a silent no-op: it is neither a revert nor a failure event. The `maxWithdraw` read is not inside a `try` block, so a revert of that call propagates.

When called as `exit`, the Alpha decides the limit: a non-zero `maxSharesBurned` makes the fuse revert with `Erc4626SupplyFuseExcessiveSharesBurned` if more shares were burned. A failed vault `withdraw` is not caught on this path either, so the whole action reverts.

## Transient variants

`enterTransient()` and `exitTransient()` take no arguments. They read their inputs from transient storage under the key `VERSION`, call the same `enter` or `exit` logic and write the result back to transient storage.

```solidity
    function enterTransient() external {
        bytes32[] memory inputs = TransientStorageLib.getInputs(VERSION);
        address vault = TypeConversionLib.toAddress(inputs[0]);
        uint256 amount = TypeConversionLib.toUint256(inputs[1]);
        uint256 minSharesOut = TypeConversionLib.toUint256(inputs[2]);

        uint256 suppliedAmount = enter(Erc4626SupplyFuseEnterData({vault: vault, vaultAssetAmount: amount, minSharesOut: minSharesOut}));

        bytes32[] memory outputs = new bytes32[](1);
        outputs[0] = TypeConversionLib.toBytes32(suppliedAmount);
        TransientStorageLib.setOutputs(VERSION, outputs);
    }
```

Source: [`Erc4626SupplyFuse.sol#L120-L131`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L120-L131)

`exitTransient` follows the same pattern with `maxSharesBurned` as the third input.

## instantWithdraw

`instantWithdraw` is optional. The vault calls it when a user withdrawal needs more assets than the vault holds, for every fuse configured with `configureInstantWithdrawalFuses`, in the configured order, until the missing amount is covered. The first parameter is always the amount in the underlying asset; the other parameters belong to the fuse. For the ERC4626 fuse `params[1]` is the vault address:

```solidity
    /// @dev params[0] - amount in underlying asset, params[1] - vault address
    function instantWithdraw(bytes32[] calldata params_) external override {
        uint256 amount = uint256(params_[0]);

        address vault = PlasmaVaultConfigLib.bytes32ToAddress(params_[1]);

        _exit(Erc4626SupplyFuseExitData(vault, amount, 0), true);
    }
```

Source: [`Erc4626SupplyFuse.sol#L150-L157`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L150-L157)

The vault does not wrap this call in `try`/`catch`, so a revert inside `instantWithdraw` reverts the user withdrawal. That is why the example catches the one call that can fail because of the venue, the vault `withdraw`, and reports the failure with an event instead:

```solidity
        if (catchExceptions_) {
            try IERC4626(vault_).withdraw(finalVaultAssetAmount_, address(this), address(this)) returns (
                uint256 shares_
            ) {
                if (maxSharesBurned_ > 0 && shares_ > maxSharesBurned_) {
                    revert Erc4626SupplyFuseExcessiveSharesBurned(shares_, maxSharesBurned_);
                }
                shares = shares_;
                emit Erc4626SupplyFuseExit(VERSION, IERC4626(vault_).asset(), vault_, finalVaultAssetAmount_, shares);
            } catch {
                /// @dev if withdraw failed, continue with the next step
                emit Erc4626SupplyFuseExitFailed(VERSION, IERC4626(vault_).asset(), vault_, finalVaultAssetAmount_);
            }
```

Source: [`Erc4626SupplyFuse.sol#L186-L198`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L186-L198)

Three things to note about this example:

- Only the vault `withdraw` is inside `try`. The substrate check and the `maxWithdraw` read in `_exit`, and the `asset()` reads for the events in both branches, are outside the `catch`, so this path can still revert. The `Erc4626SupplyFuseExcessiveSharesBurned` check cannot fire here, because `instantWithdraw` passes `maxSharesBurned = 0`.
- `instantWithdraw` passes `maxSharesBurned = 0`, so on the automatic instant-withdraw path the limit on burned shares is off. This differs from `exit`, where the Alpha can set a limit.
- A failed withdraw emits `Erc4626SupplyFuseExitFailed` and continues; a zero amount or a zero `maxWithdraw` returns `0` without any event.

## Events and errors

Events carry the fuse `VERSION` as the first parameter and are named with the fuse name as prefix. Errors are custom errors with the same prefix. The example declares no `indexed` parameters; IPOR requires the same of new fuses.

```solidity
    event Erc4626SupplyFuseEnter(address version, address asset, address vault, uint256 vaultAssetAmount);
```

Source: [`Erc4626SupplyFuse.sol#L46`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L46)

```solidity
    error Erc4626SupplyFuseUnsupportedVault(string action, address asset);
```

Source: [`Erc4626SupplyFuse.sol#L69`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626SupplyFuse.sol#L69)

The enter event reports the amount after limiting it to the available balance, so it shows what happened and not only what was requested. The example also declares `Erc4626SupplyFuseExit`, `Erc4626SupplyFuseExitFailed`, `Erc4626SupplyFuseInsufficientShares` and `Erc4626SupplyFuseExcessiveSharesBurned`. The NatSpec on the `enter`, `exit` and transient functions of this example is minimal; the Aave V4 fuses show the level of NatSpec described in the [Coding Standard](https://ipor.io/docs/build-on-fusion/integrating-a-new-market/coding-standard.md).

## Substrates and vault configuration

A [substrate](https://ipor.io/docs/build-on-fusion/atomists/vault-configuration-step-by-step/substrates.md) is a `bytes32` value that an atomist grants to a market with `grantMarketSubstrates(marketId, substrates)`. Each call replaces the whole set of substrates of that market. A fuse checks the substrate before it moves funds. A plain address is checked with `PlasmaVaultConfigLib.isSubstrateAsAssetGranted`, and a richer encoding with `isMarketSubstrateGranted`. When a substrate carries more than an address, put the encoding and decoding in a substrate library, as `AaveV4SubstrateLib` does.

The usual order for an atomist is: add the fuses with `addFuses`, set the balance fuse, grant the substrates, make sure the vault's price manager or the shared [Price Oracle Middleware](https://ipor.io/docs/build-on-fusion/developer-guide/price-oracle-middleware.md) has a price source for every asset, add a claim fuse, if any, to the vault's `RewardsClaimManager` with `addRewardFuses`, and then optionally configure instant withdraw and the [dependency balance graph](https://ipor.io/docs/build-on-fusion/atomists/curating-a-fusion-vault/dependency-graphs.md) with `updateDependencyBalanceGraphs`. The [What a Market Integration Contains](https://ipor.io/docs/build-on-fusion/integrating-a-new-market/market-components.md) section lists these components in detail.

## The balance fuse

Each market needs a balance fuse. It implements `IMarketBalanceFuse`:

```solidity
interface IMarketBalanceFuse {
    /// @notice Get the balance of the Plasma Vault in the market in USD
    /// @dev Notice! Every Balance Fuse have to implement this exact function signature, because it is used by Plasma Vault engine
    /// @return balanceValue The balance of the Plasma Vault in the market in USD, represented in 18 decimals
    function balanceOf() external returns (uint256 balanceValue);
}
```

Source: [`IMarketBalanceFuse.sol#L5-L10`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/IMarketBalanceFuse.sol#L5-L10)

`balanceOf()` returns the balance of the vault in the market in USD with 18 decimals (WAD). Every balance fuse must use this exact signature, because the vault engine calls it. The interface does not declare `balanceOf()` as `view`, although the reference implementations do. You may declare it `view`, as they do. The vault calls it by `delegatecall` and does not catch a revert, so a reverting balance fuse blocks the balance update.

`Erc4626BalanceFuse` reads the substrates with `PlasmaVaultConfigLib.getMarketSubstrates(MARKET_ID)`. For each vault it converts the shares held by the vault to assets, prices the asset through the Price Oracle Middleware and adds the value in WAD. It returns `0` immediately when the market has no substrates.

```solidity
        for (uint256 i; i < len; ++i) {
            vault = IERC4626(PlasmaVaultConfigLib.bytes32ToAddress(vaults[i]));
            vaultAssets = vault.convertToAssets(vault.balanceOf(plasmaVault));
            asset = vault.asset();
            (price, priceDecimals) = IPriceOracleMiddleware(priceOracleMiddleware).getAssetPrice(asset);
            balance += IporMath.convertToWad(vaultAssets * price, IERC20Metadata(asset).decimals() + priceDecimals);
        }

        return balance;
```

Source: [`Erc4626BalanceFuse.sol#L70-L78`](https://github.com/IPOR-Labs/ipor-fusion/blob/632350d6a31afbe7a306f0e6458bb4794c18c624/contracts/fuses/erc4626/Erc4626BalanceFuse.sol#L70-L78)

This fuse does not check for a zero price itself; the middleware rejects a zero or negative price.

`convertToAssets` ignores withdrawal limits, as the valuation rule asks, but it also leaves out fees that an ERC4626 vault charges on withdrawal (EIP-4626). For a venue that charges an exit fee, value the position at what the exit pays; see [Security Requirements](https://ipor.io/docs/build-on-fusion/integrating-a-new-market/security-requirements.md).

`ZeroBalanceFuse` is a balance fuse that always returns `0`. It is only for a market that creates no separate position of its own, with the position valued on another market. Never use it for a position that you cannot value, because it hides the position from the vault balance.

## Where to go next

- [Integrating a New Market](https://ipor.io/docs/build-on-fusion/integrating-a-new-market.md): the process, from writing the fuses to the Fuse Whitelist Registry.
- [What a Market Integration Contains](https://ipor.io/docs/build-on-fusion/integrating-a-new-market/market-components.md): every contract a market integration can contain.
- [Coding standard](https://ipor.io/docs/build-on-fusion/integrating-a-new-market/coding-standard.md) and [security requirements](https://ipor.io/docs/build-on-fusion/integrating-a-new-market/security-requirements.md): the rules for the fuses you write.
- [Whitelist Review](https://ipor.io/docs/build-on-fusion/integrating-a-new-market/whitelist-review.md): what to prepare for the review of the deployed code.
