Profit and the high water mark
Measuring equity#
sweep_profit is permissionless, and the first thing it does is work out what the vault is worth. Equity is the bankroll's own token balance plus every registered venue account, and the caller has to supply all of them, as pairs of [allowlist entry, token account], in ascending token-account order, with no repeats.
equity = bankroll.amount
+ sum over every registered venue account:
Local ──▶ the token account's balance, read directly
Remote ──▶ VenueAccount.remote_equity, the attested figureThe ordering requirement is not cosmetic. The instruction checks that the number of supplied accounts is exactly venue_count * 2 and that each token account key is strictly greater than the last, so the set can be neither padded with duplicates nor cherry picked to understate how much is already deployed. Each allowlist entry's PDA is re-derived from ["venue", policy, token_account] and compared, so a balance that is not this policy's cannot be passed off as one.
Scroll the chart sideways to see all of it.
The mark ratchets up at every sweep and never falls, so a loss has to be earned back before anything is distributed again.
The three-way split#
If equity is at or below the mark, the call reverts with NothingToDo. Above it, the excess is profit and splits three ways by the policy's own basis points, which must sum to 10000.
| Share | Goes to | What happens next |
|---|---|---|
profit_to_lp_bps | lp_quote | picked up by compound_lp and pushed into the liquidity position |
profit_to_holders_bps | holder_rewards | spent by buyback_and_burn buying the token back, which is then burned |
profit_to_bankroll_bps | nowhere | it is already in the bankroll, so it simply stays and keeps working |
Holders benefit through a smaller supply, never through a payment. There is no instruction that credits a wallet.
When the bankroll cannot cover the split#
Equity includes money sitting at venues, but only the bankroll's own liquid balance can actually move during the sweep. If the LP and holders' shares together ask for more than that balance, both are shrunk in place, proportionally, and the bankroll's own share absorbs the shortfall.
requested = to_lp + to_holders
if requested > bankroll.amount:
to_lp = to_lp * bankroll.amount / requested
to_holders = bankroll.amount - to_lp
moved = to_lp + to_holders # never more than the liquid balanceThe ratio between the LP and holders' legs is preserved exactly; only their absolute size shrinks. to_holders is computed as the remainder so the two always add back to the liquid balance with no unit lost to truncation.
Moving the mark#
new_hwm = equity - moved
The mark is set to the equity that is actually left, not to the equity that was measured. The next call therefore sees zero profit until there is a real new gain, which is what makes a double sweep impossible: nothing that has already been paid out can be counted as profit again.
The mark never falls. After a loss, equity has to climb back above the previous mark before another sweep does anything, so a vault that loses money and recovers pays out on the new gain only.
A fully worked example#
Every figure below is read from a policy on the local validator through the indexer, or is the arithmetic the program applies to those figures. The policy is ExZAh5fTMWjTFgepEC24HcmyM35ftiZskypRNaPa7GBA, Passive mode, quote at 8 decimals, profit split 3000 LP / 5000 holders / 2000 bankroll.
| Quantity | Base units | How it arises |
|---|---|---|
| Bankroll, liquid | 399,000,000 | what was left in the bankroll token account |
| Deployed at the venue | 666,000,000 | one Local venue account, asset class STOCK, read directly |
| Equity | 1,065,000,000 | the sum of the two |
| Previous high water mark | 0 | never swept before |
| Profit | 1,065,000,000 | equity minus the mark |
| Requested for LP, 3000 bps | 319,500,000 | before the shortfall rule |
| Requested for holders, 5000 bps | 532,500,000 | before the shortfall rule |
| Requested total | 852,000,000 | more than the 399,000,000 liquid, so both shrink |
| Actually moved to LP | 149,625,000 | 319,500,000 x 399,000,000 / 852,000,000 |
| Actually moved to holders | 249,375,000 | 399,000,000 minus the LP leg |
| New high water mark | 666,000,000 | 1,065,000,000 minus the 399,000,000 moved |
"hwm": { "value": "666000000", "source": "chain:Policy account" },
"counters": {
"profitSwept": { "value": "399000000", "source": "chain:Policy account" },
"paidToHolders": { "value": "0", "source": "chain:Policy account" }
},
"pdaBalances": [
{ "kind": "bankroll", "balance": { "value": "0" }, "decimals": 8 },
{ "kind": "quote_escrow", "balance": { "value": "434625000" }, "decimals": 8 },
{ "kind": "holder_rewards", "balance": { "value": "249375000" }, "decimals": 8 }
]- The bankroll is at zero: the shortfall rule moved its entire liquid balance, and the 20% bankroll share of profit is represented by the 666,000,000 still sitting at the venue.
holder_rewardsholds exactly the 249,375,000 computed above, staged and not yet spent.quote_escrowholds 434,625,000, which is the 285,000,000 the fee split left there plus the 149,625,000 from this sweep. This capture predates a change. Both legs now land inlp_quoteinstead:routemoves the LP share out of the escrow, andsweep_profitpays its LP leg there rather than into the escrow, so on the current program the same run would show an emptyquote_escrowand 434,625,000 inlp_quote. The arithmetic is unchanged; only the account holding the result is. See the fee split.paidToHoldersis still zero, because it counts quote actually spent buying the token back. Landing inholder_rewardsis staging, not payment.
Two numbers are both called profitSwept#
Policy.profit_swept accumulates moved, meaning what physically left the bankroll: 399,000,000 here. The /analytics day series reports profitSwept by summing the profit field of indexed ProfitSwept events, meaning the whole amount above the mark: 1,065,000,000 here. Both are correct for what they measure, and they differ whenever the shortfall rule bites. See the analytics endpoint.
The bankroll's own share never moves#
There is no transfer for profit_to_bankroll_bps. The share is defined as what is left after the other two legs leave, so a policy with profit_to_bankroll_bps = 10000 simply never transfers anything during a sweep, and the only observable effect of the call is that the high water mark rises to the current equity. That is still meaningful: it locks in the gain so that a later loss and recovery pays out nothing until the mark is beaten again.
When a sweep refuses to run#
| Condition | Error |
|---|---|
| Equity is at or below the high water mark | NothingToDo |
| Not every registered venue account was supplied, or they were out of order, or repeated | VenueSetIncomplete |
| A supplied allowlist entry does not derive to the expected PDA, or belongs to another policy | VenueNotAllowed |
| A Local venue account is held in a mint other than the policy's quote mint | WrongMint |
| A Remote attestation is older than max_remote_staleness | StaleRemoteEquity |
| A Remote attestation is dated in the future | FutureAsof |
| The policy is paused, including by a tripped deviation guard | PolicyPaused |
| The protocol is paused | ProtocolPaused |