All articles
Shadow ArenaOctober 29, 20264 min read

Flux CASH redemptions: a correct payout can use stale state

Trace two CASH redemption batches through a refund-adjusted denominator, then use no-refund and single-batch controls to isolate the historical persistence defect.

By Carlos (Bloqarl)

TL;DR

  • A first settlement batch can calculate the right refund-adjusted denominator and pay the right amount.
  • If that adjustment lives only in a local variable, the next transaction reloads the old denominator.
  • Compare two batches, a single batch and a no-refund control. They exercise different state histories.
  • Keep the distribution numerator and denominator on the same basis; subtracting every processed claim is not automatically the fix.

Flux CASH batch settlement needs a two-transaction trace

A payout formula can be right while its input is stale. Reading the multiplication and division once will not reveal which storage state the next settlement transaction uses.

The historical CashManager source processes selected refunds, subtracts the refunded amount from an epoch total in a local variable, and passes that local denominator into the redemption loop. The original Ondo contest report maps the missing persisted adjustment to H-01. This walkthrough concerns that pinned historical code.

Map obligations before calculating payouts

Start with requestRedemption. A request adds to the user's pending claim and the epoch's aggregate burned amount, then burns their CASH.

The later admin settlement has two different paths. A refund clears a pending claim and remints CASH. A redemption clears a claim and transfers collateral according to its fraction of the distribution basis. Both paths change user claims; that does not imply that both should reduce the denominator in the same way.

Write down three columns before stepping through the calls: stored epoch total, local denominator and each user's pending claim. Keep CASH amounts separate from USDC amounts.

Predict the second batch before reading it

Consider 450 CASH burned in an epoch: Alice 200, Bob 200 and a third user's 50. Refund the third user in the first settlement call. Use a distribution basis of 300 USDC, with zero fees for this example.

The first call calculates an effective denominator of 400 CASH. Alice's 200 CASH claim receives 150 USDC. Her pending claim is cleared. The refundee's claim is also cleared, but the epoch total remains 450 in storage.

In the next call, there is no new refund to subtract. The local denominator is therefore 450, and Bob's equally sized claim receives only 133.333333 USDC after six-decimal truncation. Persisting the refund-adjusted 400 denominator instead gives 150 USDC.

Point in the traceStored CASH totalLocal denominatorResult
Before refund450Not yet computedThree pending claims
First call after refund450400Alice receives 150 USDC
Second call, historical code450450Bob receives 133.333333 USDC
Second call with persisted refund adjustment400400Bob receives 150 USDC

The 300 USDC input is the same distribution basis in these calls, not a claim that 600 USDC should actually leave the sender. Each selected recipient receives their computed portion. Changing that input between calls changes the example and must be reasoned about separately.

Use controls that expose the cause

Settle both redeemers in the same call after the refund: both use the same local 400 denominator. That happy case can pass despite the missing persistent adjustment.

Now remove the refund and keep the 450 CASH denominator for both calls. There is no missing refund adjustment to expose. A multi-call test without a refund can also miss this specific defect.

Finally, propose subtracting Alice's paid 200 from the denominator before Bob's batch, while retaining the same distribution basis. Bob would receive a different fraction again. That is not equivalent to recording the refund adjustment; it changes the accounting model. Trace the numerator alongside the denominator before treating it as a repair.

These are static worked predictions, not a newly executed Solidity test or a statement about current production behavior. An executable reproduction must also exercise roles, epoch eligibility, token transfers and fees.

Transfer the review habit

For every local total derived from persistent state, ask what the next transaction reloads. Does a consumed claim stay consumed? Does the aggregate still describe the same distribution basis? Which transitions must survive after this call returns?

The share-rounding walkthrough separates underlying cash from share supply. The KYC walkthrough separates a stored approval bit from a still-valid signed approval. Here the distinction is a locally adjusted number versus the stored number used by the next batch.

Use the Flux Finance exercise to write your own state table before opening the reviewed results. A useful finding explains the failing sequence and the control that stays correct, rather than only pointing at a missing storage assignment.

Tagged

Shadow ArenaFlux FinanceState Transitions