For the complete documentation index, see llms.txt. This page is also available as Markdown.

How profit and loss reach your shares

distributeYield(uint256 amount, uint256 feeAmount, uint256 nonce, bool isProfit)

Callable only by the Rebalancer, on the Yield contract. It is the function that determines what your shares are worth, so it is worth understanding precisely.

What gets reported

Parameter
Meaning

amount

The shareholders' slice of realised P&L

feeAmount

The treasury's slice, on top of amount — not carved out of it

nonce

Epoch number; must equal epoch + 1 exactly

isProfit

Direction of the report

feeAmount being additive rather than deductive is the detail most people get wrong. A report of amount = 1000, feeAmount = 500 means the strategy realised 1,500 in total: 1,000 to depositors, 500 to the treasury. The vault books both.

What is checked before anything is booked

Guard
Effect

amount <= totalAssets() * maxYieldPercentage / 10_000

Caps report size — default 50 bps (0.5% of assets)

feeAmount <= amount * maxFeePercentage / 10_000

Caps the fee slice — default 5,000 bps (50% of amount)

nonce == epoch + 1

Strict sequencing; the primary replay defence

trxns[proofHash]

Duplicate rejection, keyed on (vaultToken, amount, nonce, isProfit)

lastDistributionTime + minDistributionInterval <= block.timestamp

Rate limit — default 79,200s (~22 hours)

Note that feeAmount is not part of proofHash, so it does not contribute to the replay identity — the nonce check is what actually prevents a report being submitted twice.

The accounting the share price is derived from

Two storage values do all the work:

virtualAssets        — every asset the vault believes it is owed
getUnvestedAmount()  — profit reported but not yet released

totalAssets()        = virtualAssets - getUnvestedAmount()
share price          = totalAssets() / totalSupply()   (with ERC-4626 virtual-share offset)

totalAssets() is an accounting figure, not a token balance, and is never reconciled against one. Profit is added to virtualAssets immediately but withheld from totalAssets() until it vests — which is what makes the share price rise gradually rather than step.

On profit

1

Outstanding treasury debt is settled first

If treasuryDebt > 0, up to feeAmount of it is cleared and that portion is redirected to depositors instead of being paid to the treasury. Shareholders receive amount + repaid. The treasury earns nothing new while it owes.

2

The shareholders' slice enters the vesting schedule

transferInRewards(amount + repaid, true) adds the profit to virtualAssets and sets vestingAmount, then stamps lastDistributionTimestamp = block.timestamp.

From there getUnvestedAmount() decays linearly to zero across vestingPeriod, releasing the profit into totalAssets() as it goes.

3

The treasury's fee is minted as shares, split across the schedule

previewProfitFee(netFee) returns three values:

  • feeShares — priced against the fully vested base (virtualAssets), rounded up, so they are worth exactly netFee once the schedule completes

  • recognisedFee — the most that can enter totalAssets() right now without the mint moving the share price, rounded down

  • vestingFee — the remainder, added to the same vesting schedule as everyone else's profit

The treasury is paid in full, but on the same timetable as depositors rather than ahead of them. Pricing the shares against the recognised base instead would hand the treasury extra shares that then capture part of the depositors' profit as it vests.

On loss

Losses are applied instantly, with no vesting — the assets are already gone, so withholding the write-down would leave the vault overstating itself while redemptions kept settling against the inflated price.

1

Unvested profit absorbs the loss first

_absorbLossIntoUnvested(amount) cancels the loss against profit that has not vested yet. That profit has been credited to nobody, so destroying it costs no holder anything.

If the loss fits entirely inside the unvested buffer, the recognised share price does not move at all:

The surviving profit keeps its original vesting end date — vestingAmount is rescaled against the elapsed fraction and lastDistributionTimestamp is deliberately left untouched, rather than restarting the clock.

2

The remainder reduces the share price immediately

Whatever the buffer could not absorb is subtracted from virtualAssets, and the share price drops in the same transaction.

3

The treasury burns its own shares to absorb its slice

feeAmount worth of treasury shares are burned — the mirror of the fee it earns on profit.

The burn is clamped to the treasury's actual balance. Treasury shares are ordinary ERC-20 shares it can transfer or redeem at any time, so burning unconditionally would revert the entire loss report whenever the treasury had been swept — leaving the vault priced above its real value while redemptions kept settling.

4

Any shortfall becomes treasuryDebt

If the treasury could not cover its slice, the uncovered portion is written down against depositors now (the assets are gone regardless) and recorded as treasuryDebt rather than being silently socialised. Future performance fees repay it before the treasury is paid again.

Last updated