DeFi Accounting
Day 5··13 min read

Reconciliation and Adversarial Accounting

Closing Module 1 by reconciling every asset and share, testing complete operation sequences, and fixing a zero-share vault windfall.

View the tested implementation

The first four days built the accounting pieces: proportional ownership, fixed-point rounding, time-based interest, and fee crystallization. Day 5 asks whether those pieces remain correct when they are composed.

After a long sequence of operations, can every asset and every share still be explained?

The answer now has an executable audit, a complete multi-user lifecycle, adversarial state-machine tests—and one real bug that had survived the previous 158 tests.

The final Module 1 implementation and its 198 passing tests live in the DeFi accounting repository.

Reconciliation means explaining the state twice

A vault has two related ledgers.

The asset ledger explains changes in economic value:

A_end = A_start + deposits + interest + profit − withdrawals − losses − asset-paid fees

The share ledger explains changes in ownership:

S_end = S_start + deposit shares + minted shares + fee shares − withdrawal shares − redeemed shares

Our manager fees are paid by minting shares, so they appear in the share ledger without removing assets. Existing users are diluted while the underlying capital remains invested.

The second ledger has an exact identity:

total shares = sum of all tracked account balances

If total supply says 12,000 while account balances sum to 11,950, then 50 shares are unexplained. They were incorrectly minted, burned, or recorded. Either way, the accounting is broken.

Claims reconcile with an inequality

An account holding sᵢ shares in a vault with A assets and S total shares has a claim:

claim_i = floor(s_i × A / S)

ERC-4626 defines shares as proportional claims on underlying assets and requires asset conversions to round down (ERC-4626). Because every account is rounded independently:

sum of individual claims ≤ total assets

The difference is reconciliation dust:

dust = total assets − sum of individual claims

Suppose a vault has 101 assets and 100 shares distributed as 40, 35, 24, and 1. The independently floored claims are 40, 35, 24, and 1. They sum to 100, leaving one unit of dust.

That asset is not unowned. It remains collectively represented by the share system. Quietly sending it to a treasury would reduce total assets—and therefore every holder's economic claim—without burning shares or following a declared fee policy.

The dust bound

For each positive-share account, flooring discards a fractional remainder in the interval [0, 1). With n positive claimants:

0 ≤ reconciliation dust < n

This is a strict bound, not an estimate. The sum of n discarded fractions must remain below n whole asset units.

The audit reports that dust; it does not try to distribute or sweep it.

Snapshot claims and sequential redemption

The four claims in the 101-asset example were calculated against one snapshot. Actual redemptions mutate both assets and shares.

If the 40-, 35-, and 24-share holders redeem sequentially, they receive 40, 35, and 24 assets. The remaining state becomes:

A = 2 · S = 1

The final share then redeems for both remaining assets. The last holder absorbs the accumulated remainder because they now own the entire residual vault.

This is not a treasury windfall. It is the consequence of repricing after each state transition. A complete final redemption must leave:

A = 0 · S = 0

Ordering settles old economics before new capital

Imagine Alice owns 1,000 shares backed by 1,000 recorded assets. Another 100 assets of interest have economically accrued but have not yet been applied. Bob deposits 110.

The correct sequence is:

accrue interest → crystallize fees → calculate price → execute operation

After accrual, the price is 1.10, so Bob receives 100 shares. Alice keeps the full 100 assets of yield earned before Bob arrived.

If Bob is priced using the stale price of 1, he receives 110 shares. Once the missing interest is recognized, the state becomes 1,210 assets and 1,110 shares:

price = 1,210 / 1,110 ≈ 1.09009

Bob's 110 shares can claim about 119.91 assets. He deposited 110 and captured roughly 9.91 of Alice's historical yield.

No asset was created or destroyed. Bad ordering redistributed ownership.

The rule that connects Days 1–5 is therefore:

Settle existing economics before pricing new capital.

Preview, validate, commit

A timestamped capital operation is treated as one compound transition:

  1. Accrue interest to the requested timestamp.
  2. Crystallize pending management and performance fees.
  3. Price the deposit, mint, withdrawal, or redemption.
  4. Validate arithmetic, balances, slippage, timestamps, and structural invariants.
  5. Commit the resulting state only if every stage succeeds.

If the last stage fails, the earlier prospective work must disappear as well. A rejected withdrawal cannot still advance an interest checkpoint, mint fee shares, update the high-water mark, or change a management-fee base.

This mirrors the execution model described in Solana's official documentation: instructions inside a transaction execute sequentially and atomically (Writing to the Network).

A read-only audit layer

Day 5 adds two public interfaces:

  • Vault::accounts() iterates over the balances already stored by the vault. There is no second user registry that could drift from the actual share ledger.
  • Vault::audit() derives a checked, immutable VaultAudit report without changing any state.

The report contains total assets and shares, the sum of tracked balances, the sum of independently floored claims, reconciliation dust, fee-recipient ownership without double-counting, fixed-point share price, checkpoint information, and a final structural-invariant result.

Calling the audit repeatedly must return the same result and leave the complete vault unchanged. Checked u128 arithmetic turns an unrepresentable audit calculation into an explicit error rather than a wrapped answer (Rust u128).

The audit is intentionally a point-in-time snapshot. It explains current state; it is not a historical event ledger or an external proof that the vault's reported assets correspond to real tokens.

The bug found by the state machine

The new operation-driven test quickly found a bug that had existed since the original withdrawal implementation.

withdraw takes an exact asset amount and rounds the required shares upward:

shares burned = ceil(assets requested × total shares / total assets)

That direction normally protects remaining holders. But consider a vault with one share worth 10,098 assets. The holder asks to withdraw only one asset:

ceil(1 × 1 / 10,098) = 1 share

The calculation burns the vault's entire share supply while transferring only one asset. The resulting state is:

A = 10,097 · S = 0

The old empty-vault branch treated S = 0 as a fresh vault and priced the next deposit one-for-one. If Bob then deposited 100, he received 100 shares backed by 10,197 assets. Bob captured the stranded 10,097 as a free windfall.

Every local formula did what it was written to do. Their composition violated ownership conservation.

Why redeem is the correct full exit

The fix rejects a withdrawal whenever it would burn the final share while leaving assets behind:

AccountingError::WithdrawWouldStrandAssets { remaining_assets }

The operation fails atomically. Nothing is burned and nothing is transferred.

This does not prevent a clean exit. A holder who wants to surrender all remaining shares should use redeem. When the supplied shares equal total shares:

assets returned = floor(S × A / S) = A

The division is exact, so a final full redemption empties both ledgers together. A withdrawal that happens to empty both assets and shares is still accepted.

Testing sequences instead of isolated methods

An example test asks whether one deposit returns the expected number of shares. A state-machine test generates transitions, applies them in sequence, and checks invariants after every step.

The Day 5 generator mixes:

  • Deposits, exact-share mints, exact-asset withdrawals, and redemptions.
  • Profit, loss, and interest accrual.
  • Interest-rate and fee-rate changes.
  • Fee crystallization and fee-recipient behavior.
  • Same-timestamp operations and backwards-time failures.
  • Deliberately invalid amounts, balance requests, and slippage bounds.

Before every transition, the test snapshots the complete vault. A successful transition must reconcile. A failed transition must compare equal to the snapshot in every field.

Proptest's state-machine guidance describes the same approach: generated transitions exercise a system under test against persistent invariants, and failing sequences shrink toward a smaller counterexample (Proptest state-machine testing). In this repository, that shrinking process turned a long random sequence into the one-share withdrawal bug above.

The complete lifecycle

The deterministic Day 5 scenario takes one vault through the full module:

accrual → fair deposit → profit → fees → exact mint → loss → withdrawal → recovery → new high → partial redemption → wind-down

It includes Alice, Bob, Carol, and the fee recipient. At each transition it checks assets, shares, balances, claims, dust, interest state, fee checkpoints, and the high-water mark.

Recovery to the existing high-water mark produces no performance fee. Only value above that mark becomes eligible. A partial redemption deliberately leaves exactly two units of reconciliation dust. Sequential final redemptions then absorb the remainder through normal repricing.

The final state is exact:

total assets = 0
total shares = 0

Nothing is stranded and nothing is unexplained.

The invariant sheet

The full model now checks these rules:

  • Every outstanding share belongs to exactly one tracked account.
  • Independently floored claims never exceed vault assets.
  • Reconciliation dust stays strictly below the number of positive claimants.
  • Fair deposits do not create performance profit.
  • Depositors cannot claim historical yield or inherit historical fees.
  • Exiting users cannot escape already accrued fees.
  • Profit and interest cannot reduce share price.
  • Loss cannot increase share price.
  • Call frequency cannot manufacture interest.
  • Performance fees remain zero at or below the high-water mark.
  • Recovery to the high-water mark is not charged as new profit.
  • Repeated crystallization without an economic change is idempotent.
  • Floor-rounded fee shares cannot realize more than their entitlement.
  • Previews and executions agree when state is unchanged between them.
  • Every failed compound operation preserves every field.
  • A final full redemption cannot strand assets in a zero-share vault.

There is deliberately no global share-price monotonicity invariant. Price should rise with profit or interest, fall with loss or fee dilution, and move within documented bounds under rounding. A useful invariant describes permitted economics rather than forbidding them.

Verification

Day 5 added 40 tests:

  • 11 library tests: nine audit cases and two withdrawal-stranding regressions.
  • 12 reconciliation scenarios, including the full lifecycle and adversarial timing cases.
  • 17 reconciliation properties, including the operation-driven state machine.

The repository now contains 198 tests. The full suite passed five repeated runs, while every property suite also passed at 2,000 generated cases. All 158 tests from Days 1–4 remain unchanged and green.

The one behavior change is intentionally narrow: an exact-asset withdrawal that would destroy the final share while leaving assets behind now fails. That is a correction to an unsafe state transition, not a new fee or pricing policy.

What Module 1 established

Over five days, the vault moved from three integers to a complete accounting model:

  • Assets and shares represent value and proportional ownership.
  • Deposit, mint, withdraw, and redeem require different rounding directions.
  • Zero-result operations, slippage, overflow, and inflation attacks need explicit defenses.
  • Interest belongs in an index that user activity cannot re-anchor.
  • APR, APY, compounding, and annualization describe different things.
  • Management and performance fees need explicit bases, timing, and rounding policies.
  • A per-share high-water mark prevents deposits and loss recovery from being charged as performance.
  • Asset and share ledgers must reconcile independently.
  • Dust is bounded residual ownership, not free treasury revenue.
  • Correct methods can still compose into an incorrect state machine.

The final lesson is the one that found the bug:

Local correctness is necessary. Sequence-level economic invariants are what make the system trustworthy.

Module 2 moves from accounting a vault to pricing a market: AMMs, liquidity, price impact, arbitrage, LP economics, and concentrated liquidity.

References