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

Reporting Profit and Loss

Description: Operational runbook for the REBALANCER_ROLE holder reporting each epoch's realised P&L.

Reporting P&L

This page is for the operator holding REBALANCER_ROLE. Depositors cannot call any of these functions — see How profit and loss reach your shares for what the reports mean from a user's perspective.

Reporting P&L is one function call. distributeYield is the only function REBALANCER_ROLE gates anywhere in the protocol:

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

Everything else below is preflight work to compute arguments that will not revert — and to avoid the one class of mistake that cannot be undone.

Before you start

YIELD=<yield proxy>
VAULT=<vault proxy>
RPC=<rpc url>

Step by step

1

Read the current epoch

cast call $YIELD "epoch()(uint256)" --rpc-url $RPC

nonce must equal epoch + 1 exactly. Off by one reverts with InvalidEpoch. There is no way to skip an epoch or backfill a missed one.

2

Confirm the rate limit has elapsed

cast call $YIELD "lastDistributionTime()(uint256)" --rpc-url $RPC
cast call $YIELD "minDistributionInterval()(uint256)" --rpc-url $RPC
cast block latest -f timestamp --rpc-url $RPC

Requires lastDistributionTime + minDistributionInterval <= block.timestamp, else DistributionTooFrequent. Default interval is 79,200s (22 hours).

The very first report is exempt — the check is skipped while lastDistributionTime == 0.

3

Compute the amount ceiling

cast call $VAULT "totalAssets()(uint256)" --rpc-url $RPC
cast call $YIELD "maxYieldPercentage()(uint256)" --rpc-url $RPC

amount <= totalAssets() * maxYieldPercentage / 10_000. Default maxYieldPercentage is 50 = 0.5% of recognised assets.

4

Compute the feeAmount ceiling

cast call $YIELD "maxFeePercentage()(uint256)" --rpc-url $RPC

feeAmount <= amount * maxFeePercentage / 10_000. Default 5000 = 50% of amount — not of total realised P&L.

5

Split the realised P&L into the two arguments

This is the step that permanently corrupts vault accounting if you get it wrong.

feeAmount is additive, not deducted from amount. If the strategy realised 5 USDC and the protocol takes 20%:

Correct
Wrong

amount

4e6

5e6

feeAmount

1e6

1e6

Booked

5 USDC

6 USDC against 5 real

The wrong version overstates the vault permanently. Nothing onchain will catch it — the vault has no view of strategy balances.

6

Simulate before sending

cast call $YIELD "distributeYield(uint256,uint256,uint256,bool)" \
  <amount> <feeAmount> <nonce> <isProfit> \
  --from <rebalancer> --rpc-url $RPC

A successful call means the guards pass. It does not validate that your figures are economically correct — only that they are accepted.

7

Send

cast send $YIELD "distributeYield(uint256,uint256,uint256,bool)" \
  <amount> <feeAmount> <nonce> <isProfit> \
  --rpc-url $RPC --private-key $PK
8

Verify it landed as intended

cast call $YIELD "epoch()(uint256)" --rpc-url $RPC           # incremented by 1
cast call $VAULT "totalAssets()(uint256)" --rpc-url $RPC
cast call $VAULT "getUnvestedAmount()(uint256)" --rpc-url $RPC
cast call $YIELD "treasuryDebt()(uint256)" --rpc-url $RPC    # non-zero: loss exceeded treasury

Confirm the DistributeYield event carries the figures you intended.

Every guard, in one place

Guard
Reverts with
Default

amount <= totalAssets * maxYieldPercentage / 10_000

(no reason string)

50 bps

feeAmount <= amount * maxFeePercentage / 10_000

(no reason string)

5,000 bps

nonce == epoch + 1

InvalidEpoch

—

trxns[proofHash] unseen

DuplicateTransaction

—

Interval elapsed

DistributionTooFrequent

79,200s

Caller holds REBALANCER_ROLE

AccessControlUnauthorizedAccount

—

proofHash is keyed on (vaultToken, amount, nonce, isProfit) — feeAmount is not part of the replay identity, so the nonce check is what actually prevents a duplicate submission.

Two things this role does not do

It does not move any assets

distributeYield writes accounting only. There is not a single underlying-asset transfer in Yield.sol. Reporting a profit does not bring capital back onchain.

Returning capital is Manager.transferToVault, which requires OPERATOR_ROLE. These are independent systems:

Function
Role
Effect

Books

distributeYield

Rebalancer

virtualAssets changes

Assets

transferToVault

Operator

Real tokens arrive at the vault

Reporting profit while the vault stays unfunded raises the share price without making a single redemption payable.

It cannot correct a mistake

There is no reversal path. nonce is strictly sequential, and a bad report can only be offset by an opposing report in a later epoch — itself bounded by the per-epoch cap.

A materially wrong figure could take many epochs to unwind, and throughout that window requestRedeem keeps freezing quotes at the wrong price. Those quotes are final: users who requested during the error are paid the wrong amount permanently.

Cadence

There is no required reporting frequency. The interval is a floor, not a ceiling — nothing obliges a report, and there is no staleness check anywhere.

If reporting stops, unvested profit finishes vesting and the share price freezes at its last value indefinitely, while deposits and redemption requests keep quoting against it. Onchain this is indistinguishable from a flat-performing strategy.

The 22-hour default paired with a 7-day vesting period implies daily reporting — each epoch's profit vests across roughly seven subsequent reports, so the schedules overlap and the share price moves smoothly instead of stepping.

Note that a new profit report restarts the vesting clock for profit still in flight: vestingAmount becomes new + still-unvested and the timestamp resets. Under continuous daily reporting, profit is perpetually re-spread and never fully vests, so totalAssets() structurally lags virtualAssets. This is inherent to the design.

Monitoring after each report

Signal
Read
Meaning

Epoch advanced

epoch()

Report was accepted

Vesting in flight

getUnvestedAmount()

Profit not yet in the share price

Treasury debt

treasuryDebt()

A loss exceeded what the treasury could absorb; fees now route to depositors

Redemption backlog

totalRequestedAmount() vs the vault's asset balance

Whether matured redemptions are actually payable

That last row is the one worth alerting on. It is the only onchain signal that separates a healthy vault from one whose books look fine while users cannot exit.

Last updated