Documentation

Fee Management

What is the FeeManager?

The FeeManager is an optional contract that can be attached to MultiVehicle at deployment time to automatically collect fees on vehicle performance. Once set during initialization, it cannot be changed or removed - this ensures predictability for users.

You can be implementing the same feeManager on multiple MultiVehicle contracts to ensure that the fees are collected in the same way and having the capability to change the configuration in a central place.

Key Characteristics

  • Optional: Can be deployed with or without FeeManager
  • Automated: Fees calculated and collected automatically during operations
  • Transparent: All fee rates and recipients visible on-chain
  • Flexible: Supports multiple fee recipients with configurable splits and allow fee changes within onchain limits

When to Use FeeManager

Use FeeManager for:

  • Professional strategy operators who deserve compensation for active management
  • Institutional deployments requiring fee-based revenue models
  • Strategies with significant value-add (rebalancing, optimization, risk management)

Don’t Use FeeManager for:

  • Community-first DAOs prioritizing zero-fee operations
  • Strategies relying on additional reward fees instead (see Additional Rewards)
  • Experimental or beta deployments

Fee Types

The FeeManager supports 4 distinct fee types, each calculated and applied at different points in the vehicle lifecycle:

1. Performance Fee (on Earnings)

What it is:

Fee charged on gains above the high water mark. Only charged when the strategy makes money. Collected by minting shares to FeeManager during operations.

How it works:

High Water Mark: $1,000,000
Current Assets:  $1,100,000
Earnings:        $100,000

If performance fee = 20%:
Fee = $100,000 × 20% = $20,000 (minted as shares)
New High Water Mark: $1,100,000

Calculation timing:

  1. Calculated in FeeManager.onOperations before each deposit/redeem commit
  2. Applied as share minting to FeeManager
  3. High water mark updated in FeeManager.onUpdate after operations complete

Best for:

  • Hedge fund-style fee models
  • Performance-aligned compensation
  • User-friendly (only pay when strategy profits)

Example configuration:

Fees({
    performanceFeeBps: 2000,  // 20% of earnings
    managementFeeBps: 0,
    depositFeeBps: 0,
    redeemFeeBps: 0
})

Important notes:

  • High water mark tracks totalAssets from FeeManager.onUpdate
  • If strategy loses value, no performance fees until recovering past previous high
  • Shares minted dilute existing holders proportionally

2. Management Fee (Time-Based)

What it is:

Annualized fee prorated by time elapsed since last operation. Charged regardless of performance. Collected by minting shares to FeeManager during operations.

How it works:

Total Assets:     1,000,000 USDC
Management Fee:   2% annual (200 bps)
Time Elapsed:     30 days (2,592,000 seconds)

Fee = totalAssets × managementFeeBps × timeDelta / (10000 × 31536000)
Fee = 1,000,000 × 200 × 2,592,000 / (10000 × 31536000)
Fee ≈ 1,644 USDC worth of shares minted

Calculation timing:

  1. Calculated in FeeManager.onOperations before each deposit/redeem commit
  2. Applied as share minting to FeeManager
  3. Timestamp updated in FeeManager.onUpdate after operations complete

Best for:

  • Covering ongoing operational costs (gas, infrastructure, monitoring)
  • Predictable revenue stream
  • Passive strategies with minimal rebalancing

Example configuration:

Fees({
    performanceFeeBps: 0,
    managementFeeBps: 200,  // 2% annual
    depositFeeBps: 0,
    redeemFeeBps: 0
})

Important notes:

  • Accrues continuously based on block.timestamp
  • First operation after long idle period may mint substantial shares
  • Independent of strategy performance

3. Deposit Fee (Entry Fee)

What it is:

Fee charged when users deposit assets. Deducted from shares received during unlock. Collected as shares transferred to FeeManager.

How it works:

User Deposits:     10,000 USDC
Share Price:       $1.00/share
Expected Shares:   10,000 shares

If deposit fee = 1%:
Fee = 10,000 × 1% = 100 shares
User Receives:     9,900 shares
FeeManager Gets:   100 shares

Calculation timing:

  1. Calculated in FeeManager.applyFees(configId, asset, mode) during unlock phase
  2. Applied by reducing shares paid to user
  3. Difference transferred to FeeManager

Best for:

  • Discouraging short-term deposits (“hot money”)
  • One-time setup/onboarding costs
  • Aligning incentives for long-term holders

Example configuration:

Fees({
    performanceFeeBps: 0,
    managementFeeBps: 0,
    depositFeeBps: 50,  // 0.5% entry fee
    redeemFeeBps: 0
})

Important notes:

  • Paid in shares, not base assets
  • Immediately reduces user’s position
  • Discourages rapid deposit/withdraw cycles

4. Redeem Fee (Exit Fee)

What it is:

Fee charged when users redeem shares. Deducted from assets received during unlock. Collected as base assets transferred to FeeManager.

How it works:

User Redeems:      10,000 shares
Share Price:       $1.00/share
Expected Assets:   10,000 USDC

If redeem fee = 0.5%:
Fee = 10,000 × 0.5% = 50 USDC
User Receives:     9,950 USDC
FeeManager Gets:   50 USDC

Calculation timing:

  1. Calculated in FeeManager.applyFees(configId, asset, mode) during unlock phase
  2. Applied by reducing assets paid to user
  3. Difference transferred to FeeManager

Best for:

  • Discouraging short-term withdrawals
  • Compensating for liquidity management costs
  • Protecting remaining holders from withdrawal impact

Example configuration:

Fees({
    performanceFeeBps: 0,
    managementFeeBps: 0,
    depositFeeBps: 0,
    redeemFeeBps: 50  // 0.5% exit fee
})

Important notes:

  • Paid in base assets, not shares
  • Collected immediately during redemption
  • Can be used to create “sticky” capital

Fee Recipients & Distribution

Fees can be distributed to multiple recipients with configured percentage splits.

Configuration:

FeeRecipient[] memory recipients = new FeeRecipient[](3);
recipients[0] = FeeRecipient({
    target: operatorAddress,
    shareBps: 6000  // 60% to operator
});
recipients[1] = FeeRecipient({
    target: daoTreasuryAddress,
    shareBps: 3000  // 30% to DAO treasury
});
recipients[2] = FeeRecipient({
    target: developerAddress,
    shareBps: 1000  // 10% to developers
});
// Must sum to exactly 10,000 (100%)

feeManager.setFeeRecipients(recipients);

Requirements:

  • Caller must have Roles.FEE_MANAGER_SET_FEE_RECIPIENTS role
  • Sum of all shareBps must equal exactly 10,000 (100%)
  • At least one recipient required

Operational Workflow

Step 1: Monitor Accumulated Fees

Check FeeManager’s share balance in the vehicle:

uint256 accumulatedShares = multiVehicle.balanceOf(address(feeManager));
uint256 sharePrice = multiVehicle.totalAssets() / multiVehicle.totalSupply();
uint256 feeValue = accumulatedShares * sharePrice;

// Example: 10,000 shares × $1.05/share = $10,500 in fees

Step 2: Redeem Fees to Base Assets

Fees accrue to the FeeManager as vehicle shares. Converting them to base assets is an ordinary STEAM redeem against the vehicle, created by whoever holds the shares — the FeeManager has no redeemVehicleShares or progressQuery entrypoint of its own, and there is no FEE_MANAGER_REDEEM_VEHICLE_SHARES role. Both were removed.

Create a redeem query on the vehicle in the normal way and advance it with the standard lifecycle calls (create, resume, unlock), polling state(query) rather than looping blindly. See Vehicle & The STEAM Standard for the Query struct and the state machine.

Step 3: Distribute Fees to Recipients

Once redeemed, distribute base assets to configured recipients:

// Distribute all accumulated base assets
feeManager.dispatchERC20(IERC20(baseAsset));

Behavior:

  • Transfers entire balance of specified asset
  • Splits according to configured recipient shares
  • Uses pro-rata distribution (last recipient gets remainder to avoid rounding issues)

Requirements:

  • Caller must have Roles.FEE_MANAGER_DISPATCH_ERC20 role
  • FeeManager must have non-zero balance of the asset

Configuration Management

Update Fee Rates

Fees memory newFees = Fees({
    performanceFeeBps: 1500,  // 15% (reduced from 20%)
    managementFeeBps: 150,    // 1.5% annual (reduced from 2%)
    depositFeeBps: 25,        // 0.25% (reduced from 0.5%)
    redeemFeeBps: 25          // 0.25% (reduced from 0.5%)
});

feeManager.setFees(newFees);

Requirements:

  • Caller must have Roles.FEE_MANAGER_SET_FEES role
  • New fees must not exceed maxFees set at deployment (immutable ceiling)

Important:

  • maxFees set at deployment are immutable upper bounds
  • Cannot increase fees beyond maxFees (protects users from fee increases)
  • Can always decrease fees below maximum

View Current Configuration

// Get the active fee config. fees() returns the config id alongside the rates,
// and there is a fees(bytes32 configId) overload to read a historical config.
(bytes32 configId, Fees memory currentFees) = feeManager.fees();

// Get current recipients
FeeRecipient[] memory currentRecipients = feeManager.feeRecipients();

// Get cache for a specific vehicle. Third field is the config id that applies
// to it, which is how a fee change does not retroactively reprice open queries.
// (uint128 lastTotalAssets, uint128 lastTimestamp, bytes32 applicableConfigId)
Cache memory vehicleCache = feeManager.cache(address(multiVehicle));

Each query captures the fee configId in force when it was created and settles against that, so changing fees never reprices work already in flight.

Example Fee Configurations

Configuration 1: Performance-Only (Hedge Fund Model)

Fees({
    performanceFeeBps: 2000,  // 20% of profits
    managementFeeBps: 0,      // No management fee
    depositFeeBps: 0,         // Free entry
    redeemFeeBps: 0           // Free exit
})

Use case: Aligned with user returns, only charge on gains

Configuration 2: Management-Only (Index Fund Model)

Fees({
    performanceFeeBps: 0,     // No performance fee
    managementFeeBps: 50,     // 0.5% annual
    depositFeeBps: 0,         // Free entry
    redeemFeeBps: 0           // Free exit
})

Use case: Low-cost passive strategy with minimal management

Configuration 3: Full Fee Suite (Active Management)

Fees({
    performanceFeeBps: 1000,  // 10% of profits
    managementFeeBps: 200,    // 2% annual
    depositFeeBps: 50,        // 0.5% entry
    redeemFeeBps: 50          // 0.5% exit
})

Use case: Professional active management with entry/exit barriers

Configuration 4: Entry Barrier (Long-Term Capital)

Fees({
    performanceFeeBps: 0,     // No performance fee
    managementFeeBps: 0,      // No management fee
    depositFeeBps: 100,       // 1% entry fee (discourage short-term)
    redeemFeeBps: 0           // Free exit
})

Use case: Encourage committed capital, free to exit anytime

Important Considerations

Immutability

Cannot Change After Deployment:

  • FeeManager address set during MultiVehicle construction
  • maxFees set at FeeManager deployment (ceiling for all future fee updates)
  • To change FeeManager, must deploy new MultiVehicle

Plan Carefully:

  • Choose maxFees conservatively (can always charge less, never more)
  • Example: Set maxFees.performanceFeeBps = 3000 (30%) even if starting at 2000 (20%)
  • Provides flexibility without requiring redeployment

User Impact

Fee transparency is critical:

  • All fees visible on-chain via view functions
  • Users can calculate exact fee impact before depositing
  • High water mark prevents double-charging on same gains

Consider user perception:

  • Performance fees: Most user-friendly (only charged on gains)
  • Management fees: Acceptable if low (lower than 10% annual)
  • Deposit/Redeem fees: Can feel punitive if too high (keep lower than 1%)