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
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
What is checked before anything is booked
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
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.
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.
A new profit report restarts the vesting clock for profit already in flight. vestingAmount is set to new + still-unvested, and the timestamp resets — so a previously reported profit that was partway through vesting is re-spread across a fresh full period.
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 exactlynetFeeonce the schedule completesrecognisedFee— the most that can entertotalAssets()right now without the mint moving the share price, rounded downvestingFee— 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.
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.
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.
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.
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