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:
- Calculated in
FeeManager.onOperationsbefore each deposit/redeem commit - Applied as share minting to FeeManager
- High water mark updated in
FeeManager.onUpdateafter 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
totalAssetsfromFeeManager.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:
- Calculated in
FeeManager.onOperationsbefore each deposit/redeem commit - Applied as share minting to FeeManager
- Timestamp updated in
FeeManager.onUpdateafter 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:
- Calculated in
FeeManager.applyFees(configId, asset, mode)during unlock phase - Applied by reducing shares paid to user
- 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:
- Calculated in
FeeManager.applyFees(configId, asset, mode)during unlock phase - Applied by reducing assets paid to user
- 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_RECIPIENTSrole - Sum of all
shareBpsmust 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_ERC20role - 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_FEESrole - New fees must not exceed
maxFeesset at deployment (immutable ceiling)
Important:
maxFeesset 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
maxFeesset at FeeManager deployment (ceiling for all future fee updates)- To change FeeManager, must deploy new MultiVehicle
Plan Carefully:
- Choose
maxFeesconservatively (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%)