DeFi Accounting
Day 4··14 min read

Vault Fees and High-Water Marks

Management fees, performance fees, dilution-correct share minting, and high-water marks that charge each gain only once.

View the tested implementation

Day 3 asked how a vault earns yield without letting transaction timing choose the compounding schedule. Day 4 asks the next question: once value has been created, how can the vault charge for managing it without confusing capital flows with performance or charging users twice for the same gain?

A fee rate is the easy part. The accounting policy decides what is charged, when it is charged, and who actually pays.

The complete Rust implementation and its 158 passing tests live in the DeFi accounting repository.

Two different reasons to charge a fee

A manager can charge for two fundamentally different things:

  • Management: operating a strategy and looking after capital over time.
  • Performance: producing new value for depositors.

A management fee is therefore time-based and usually tied to assets under management. A performance fee is outcome-based and should apply only to eligible profit.

Suppose a vault begins with 50,000 assets and grows to 56,000. With no management fee and a 25% performance fee:

gross profit = 56,000 − 50,000 = 6,000
performance fee = 6,000 × 25% = 1,500
net assets = 56,000 − 1,500 = 54,500

The vault produced a 12% gross return and left users with a 9% net return. If there had been no profit, the performance fee would have been zero.

Management fees need a clock and a base

Let A be the fee base, m the annual management-fee rate, and t elapsed time in years:

management fee = A × m × t

For 80,000 assets at a 1.5% annual fee over six months:

fee = 80,000 × 0.015 × 0.5 = 600

With no strategy return, the vault retains 79,400. If it also loses 4,000 during the period, it retains 75,400 after the same fee. That is economically uncomfortable but internally consistent: under this policy, management is charged even when performance is negative.

The formula is incomplete until the base convention is explicit. A protocol could use opening AUM, average AUM, continuously changing NAV, or another documented measure. This model stores an explicit management_fee_base: the assets under management at the last fee checkpoint. Capital events refresh that base when they occur. Interest accrued within an interval is not silently folded into the base until the interval is crystallized.

In fixed-point form, with an annual rate scaled by RATE_SCALE:

fee = base × rate_scaled × elapsed_seconds / (RATE_SCALE × SECONDS_PER_YEAR)

The same time rules from Day 3 still apply: a repeated timestamp charges nothing, backwards time is rejected, and a rate change settles the old interval before installing the new rate.

Deposits are not performance

Consider a vault with 100,000 assets and 50,000 shares:

share price = 100,000 / 50,000 = 2

Bob deposits 20,000 at the fair price and receives 10,000 shares. The new state is 120,000 assets and 60,000 shares, but the price remains 2.

Total assets increased by 20,000. Nobody earned a return.

performance ≠ current assets − previous assets

Performance must be measured per share because a correctly priced deposit increases assets and shares proportionally. This is the ownership model behind ERC-4626, where shares represent a proportional claim on a vault's underlying assets.

Suppose the 120,000-asset vault then grows to 144,000 while its 60,000 shares remain unchanged:

gross share price = 144,000 / 60,000 = 2.40

The profit is 0.40 per share, or 24,000 in total. At a 20% performance fee, the manager is owed 4,800—not 20% of the entire vault.

Taking assets versus minting shares

Once a fee value F has been calculated, the vault still has to pay it.

One option is to transfer assets out. If a vault has 100,000 assets and 50,000 shares, transferring a 2,000 fee leaves 98,000 assets and a post-fee share price of 1.96.

Another option is to keep every asset invested and mint shares to the fee recipient. Existing holders retain the same number of shares but own a smaller fraction of the vault.

A naive implementation might mint shares using the old price:

x_naive = F × S / A

That underpays the manager because the newly minted shares dilute themselves. The correct equation asks for the new shares x to own exactly F of value after minting:

x / (S + x) × A = F

Solving for x:

x = F × S / (A − F)

For 100,000 assets, 50,000 shares, and a 2,000 fee:

x = 2,000 × 50,000 / 98,000 = 1,020.408...

After minting, the vault still holds 100,000 assets and has approximately 51,020.408 shares. Its post-fee price is 1.96, and the manager's new shares are worth 2,000. Asset transfer and fee-share minting can therefore impose the same economic fee through different state transitions.

Enzyme's official performance-fee documentation derives the same dilution-correct formula and pays fees in shares (Enzyme performance-fee formula).

Rounding is fee policy

On-chain programs cannot mint 1,020.408 indivisible shares. This model rounds fee shares down:

x = floor(F × S / (A − F))

For 1,000 assets, 333 shares, and a fee value of 10:

x = floor(10 × 333 / 990) = floor(3.3636...) = 3

The manager receives slightly less than 10 units of value. Rounding up would silently collect more than the configured fee, so the remainder stays with holders. With realistically precise share units, that difference is usually tiny.

The implementation multiplies before dividing and widens intermediate arithmetic to u128. A nonzero fee that still rounds to zero shares is not forgotten: the operation leaves the checkpoint and high-water mark unchanged, carrying the fee into a later interval. A fee greater than or equal to all vault assets is rejected.

ERC-4626 treats fees as amounts charged in assets or shares and requires preview methods to reflect operation-specific fees accurately. Its security section also makes rounding direction an explicit part of vault behavior (ERC-4626 specification). OpenZeppelin's official ERC-4626 guide shows how fee-inclusive previews and state changes must remain consistent (OpenZeppelin ERC-4626 guide).

The high-water mark

Without memory, a performance fee can charge the same gain repeatedly.

Suppose a post-fee share price reaches 2.30, falls to 1.80, and later recovers to 2.30. The recovery has created no new wealth beyond the previous peak. Charging another performance fee would make users pay twice for the same performance.

The vault stores a per-share high-water mark, or HWM:

eligible profit per share = max(current share price − HWM, 0)

With an HWM of 2.30, 100,000 shares, a current gross price of 2.50, and a 20% performance fee:

eligible profit per share = 2.50 − 2.30 = 0.20
eligible profit = 0.20 × 100,000 = 20,000
performance fee = 20,000 × 20% = 4,000

The gross assets are 250,000. After accounting for the 4,000 fee, holders own 246,000 of net value:

post-fee share price = 246,000 / 100,000 = 2.46

Under the model's net-of-fees policy, the HWM becomes 2.46—not the gross price of 2.50. Immediately crystallizing again must charge zero because the current post-fee price equals the stored HWM.

Enzyme documents the same central rules: performance fees apply only above the high-water mark, only wealth created above that mark is eligible, and the stored mark is updated using the post-fee share price (Enzyme performance-fee principles).

Loss, recovery, and a genuinely new high

Begin from an HWM of 2.46. The price falls to 2.00, recovers to 2.40, and then returns to 2.46. Every one of those states owes zero performance fee.

If the gross price later reaches 2.60:

eligible profit per share = 2.60 − 2.46 = 0.14
eligible profit = 0.14 × 100,000 = 14,000
performance fee = 14,000 × 20% = 2,800

After the fee, assets are 257,200 and the net share price is 2.572. That becomes the new HWM. The manager is rewarded only for crossing the old net peak.

The implementation avoids calculating a truncated share price before comparing it with the HWM. Instead, it compares cross-products:

eligible numerator = max(A × RATE_SCALE − HWM × S, 0)

That keeps the comparison exact until the final division.

Crystallization and capital-flow timing

An accrued fee is an economic liability even before fee shares are minted. If Bob deposits immediately before crystallization at the gross price, a naive vault can make him pay part of a fee generated before he arrived. If an existing holder withdraws at that same gross price, they can escape part of an already-earned fee and leave remaining holders to absorb it.

This model uses a strict ordering for timestamped capital operations:

accrue interest → crystallize fees → price the capital operation

Deposits, mints, withdrawals, redemptions, profits, and losses therefore see a post-fee state. New users do not inherit historical fee liability, and exiting users cannot leave it behind.

Crystallizing on capital events is one explicit policy, not a universal standard. Other systems use scheduled periods, series accounting, or user-specific equalization. The important requirement is that the implementation and its public previews describe the same policy.

Combining management and performance fees

Suppose a vault starts with 100,000 assets and 100,000 shares. It charges a 2% annual management fee and a 25% performance fee. After six months, the strategy has produced 10,000 of gross profit.

The management fee is:

100,000 × 2% × 0.5 = 1,000

Assets after that fee are economically accounted for are 109,000. Eligible performance profit is therefore 9,000:

performance fee = 9,000 × 25% = 2,250

The combined fee value is 3,250. Instead of performing two sequential mints and accumulating two separate rounding effects, the implementation calculates the complete crystallization and mints once:

fee shares = floor(3,250 × 100,000 / (110,000 − 3,250))

The operation then updates the fee recipient's balance, total shares, checkpoint, management-fee base, and net HWM as one atomic state transition.

Preview first, mutate second

Day 4 adds public previews for:

  • Accrued management-fee value at a timestamp.
  • Eligible performance profit.
  • Performance-fee value.
  • Fee shares that would be minted.
  • The complete crystallization outcome.

The state-changing crystallization method uses the same calculations. This lets callers inspect the economic result without mutation and prevents the preview and execution paths from quietly developing different fee rules.

All arithmetic and validation complete before state is committed. If time moves backwards, multiplication overflows, the fee consumes the vault, or no recipient is configured for a nonzero fee, the entire operation fails without changing assets, shares, balances, interest state, fee checkpoints, rates, or the HWM. This matches the all-or-nothing transaction model described in Solana's core documentation.

What the tests try to break

The repository now contains 158 tests: 75 library unit tests, 12 fee property tests, 14 fee scenarios, 16 interest property tests, 7 interest scenarios, 10 vault property tests, and 24 vault scenarios.

The Day 4 tests cover:

  • Management fees across elapsed time, losing periods, and repeated timestamps.
  • Deposits that increase assets without creating performance profit.
  • Loss and recovery below a high-water mark.
  • New gains above the HWM and net-of-fees HWM updates.
  • Dilution-correct fee-share minting and coarse-unit rounding.
  • Repeated crystallization without double charging.
  • Deposits and withdrawals around fee crystallization.
  • Rate changes that settle the old interval first.
  • Overflow, backwards timestamps, missing recipients, and excessive fees.
  • Atomic failure across every affected field.
  • Compatibility with the cumulative interest index from Day 3.

The property tests run the same questions across generated states: fee-share value may not exceed the intended fee, deposits may not manufacture performance, recovery to the HWM may not generate a fee, share balances must reconcile to total supply, and failed operations may not mutate anything.

All 119 tests from Days 1–3 continue to pass without modification when fees are unused.

What I learned

  • Management fees pay for time under management; performance fees pay for eligible gains.
  • A management-fee formula is incomplete without a precise AUM-base convention.
  • Deposits increase total assets but do not increase share price, so they are not profit.
  • Minting fee shares requires solving for self-dilution; the old share price is not enough.
  • Rounding fee shares down prevents the recipient from collecting more than the configured fee.
  • A per-share high-water mark prevents total capital flows from looking like performance.
  • Loss recovery is not new profit.
  • A net-of-fees HWM makes repeated crystallization idempotent.
  • Fees must crystallize before capital is priced if entering and exiting users are to be treated fairly under this model.
  • Every fee transition has to remain compatible with interest accrual, rounding, and atomic failure.

Day 5 closes the module by reconciling the complete accounting system and attacking the interactions between deposits, withdrawals, yield, losses, rounding, fees, and time.

References