A fractional market can retire — Begin, the walk, Finish — 2026-08-30

Result

Yes, end to end, on chain, against real ELFs. a_fractional_market_retires_end_to_end_from_begin_through_finish drives four real transactions with no planted cursor and no planted intermediate state: admit the zero reserve Position while the Market is Open, resolve it, Begin, retire the sole coordinate against the audited Token-2022 v11, Finish. Six of six tests in the campaign pass; zero SBF frame diagnostics.

Measured at width 1:

actCU
Begin38,123
RetireCoordinate197,489
Finish28,190

Measured after the seam fixes at 6e689907. The same campaign read 41,678 / 207,931 / 31,558 two bases earlier and 44,678 / 203,431 / 33,058 one base earlier; the named frame role is cheaper than the three-way tuple comparison it replaced. Treat these as a few thousand units wide, not as pins.

Three things were wrong, and only the first was known

1. Begin and Finish did not exist

FRACR3's finding, sized in FRACTIONAL_CLAIM_CHECK_SIZING_2026_08_30.md: fractional_retirement_v3.rs dispatched only RetireCoordinate and refused the other two actions outright, so nothing on chain could create or close the cursor the coordinate walk advances. The contract's FractionalRetirementCursorV3::{begin, finish} existed with tests; no route called them.

2. The walk could not have taken a second step

Strictly downstream of the first, and invisible while the first was true.

authenticate_root required root.revision == request.expected_revision. cursor.advance simultaneously requires request.expected_revision == cursor.revision. The Trading-owned root is written once by the activation that creates it and mutated by no program in the tree — its revision is a constant. The cursor consumes one revision per act. So the two comparisons are satisfiable together for exactly one coordinate, and refuse every step after it.

protocol-position/tests/lifecycle.rs had papered over this by planting root.revision = 5 while calling begin with pre_revision = 4 — a root one revision ahead of the one a real Begin would have left. That works for coordinate 0 and nothing else.

The relation that actually holds needs no new persisted byte, because the 296-byte cursor is fully packed and already carries both halves of it:

cursor.revision == root.revision + 1 + cursor.next_coordinate

FractionalRetirementCursorV3::root_revision_anchor states it; RootRevisionBindingV3 names the two cases (Request at begin, CursorAnchor after) so a later route cannot pick whichever comparison was written beside it.

3. The only on-chain test of the route had stopped walking it

The selection-config weld (4630ad77) taught the fractional-atomic fixture to plant the identity a real activation writes — digest(selection_config) — and left protocol-position planting the terms record digest. Its retirement test had been refusing at 0x500B in authenticate_root ever since, satisfying its own !accepted assertion for the wrong reason and never reaching the Token CPI it exists to observe. That is why (2) went unnoticed.

What landed

Begin

16 accounts. Permissionless, like everything else in this family, because the cursor's content is fully determined by state authenticated before it runs: the terms and the root supply every field, and the rent beneficiary comes from the root. A route that demanded a signature would hand whoever held it the power to strand every shard holder's collateral behind a walk nobody else may crank.

gaterefusal
request width, magic, version, reserved bytes, action tag0x5000 Instruction
frame count, exact signer/writable/executable per named index, address distinctness0x5001 Accounts
terms record finalized under the Registry; request bound to terms0x5008 Representation
aggregate re-derived at [LIABILITY_BASIS_MARKET_SEED_V2, market] and Claims-owned0x5002 Identity
Core state PDA re-derived from its own identity; market/release/generation join0x5002 Identity
Core phase TerminalOrRetiring and terminal_receipt.is_some()0x5002 Identity
terms project to the Market-selected config0x500B SelectionConfig
root PDA, owner, bump, terms, market, rent beneficiary, frozen revision0x5008 Representation
TokenBehavior record finalized and selecting the request's Token program0x5009 Token
cursor address vacant — system-owned and empty0x5008 Representation
allocation post-check: Claims-owned, 296 bytes, funded0x5008 Representation
receipt construction and self-binding0x5007 Receipt

TerminalOrRetiring rather than Exactly(Terminal) for the reason the tree already records twice: begin_retiring is permissionless, so refusing at Retiring would let any stranger push a Market one phase forward and strand a retirement that had not started.

Anti-replay is the cursor account's own existence. A second Begin finds a Claims-owned, non-empty address and refuses at the vacancy check. No counter, no cursor about the cursor.

Finish

13 accounts, likewise permissionless. FractionalRetirementCursorV3::finish refuses unless next_coordinate == representation_width, so a cursor abandoned mid-walk cannot be closed out from under the coordinates it still owes (0x5008). The RentCredit is authenticated through the same authenticate_rent_credit the Position close uses, now taking a named LifecycleRentCreditIdentityV2 instead of a ProtocolPositionRequestV2 so both callers ask one author; a RentCredit that is not the Market's own refuses at ProtocolPositionSbfErrorV2::Rent, 0x5146. The close is the tree's five-step shape: fill(0), absolute lamport assignment, resize(0), assign(system), re-verify all four.

Two griefing surfaces, closed

Both are the same lesson protocol_position_v2 recorded for admission, arrived at from the other end — there a donation blocked entry, here it blocked the exit.

Tests

Contract (crates/dclutch-fractional-claim-contract/tests/fractional_v3.rs, 14/14)

testwhat it pins
the_root_revision_anchor_is_constant_across_the_whole_ordered_walkthe anchor over begin + 3 coordinates + finish, asserting at each step that the request revision has already diverged from the root's. This is the test that would have caught (2).
an_anchor_underflow_is_an_arithmetic_refusal_and_never_a_wrapped_revisiona forged cursor whose coordinate advanced without its revision refuses rather than wrapping
both_ends_of_the_walk_round_trip_and_bind_to_the_request_that_produced_thembegin names the root's revision and finish the cursor's; both leave expected + 1; a substituted digest and a neighbouring revision each refuse
a_lifecycle_receipt_cannot_disagree_with_itself_about_the_lamportsbegin settling anything, finish settling nothing, and finish stranding part of the principal are all unrepresentable; a donation above the principal round-trips
only_a_complete_walk_finishes_and_only_finish_claims_onethe completeness equivalence, from both sides
a_lifecycle_receipt_may_not_carry_the_coordinate_actionthe two receipt families do not share a magic and neither impersonates the other
both_lifecycle_frames_are_exact_and_below_the_coordinate_frame16 and 13, both under the lock bound

On chain (programs/dclutch-claims-sbf/program-test/protocol-position/tests/lifecycle.rs, 6/6)

testwhat it pins
a_fractional_market_retires_end_to_end_from_begin_through_finishthe whole walk; conservation in lamports at every step and once over the walk; root_revision_anchor == 4 before and after the coordinate, which is the only place (2) is observable on chain; a donated cursor makes begin's bill zero; finish settles principal plus the stray
an_incomplete_walk_cannot_be_finished_and_the_cursor_survives_the_attempt0x5008 at the revision begin leaves behind — the only one a caller could try — and the cursor byte-identical afterwards
a_second_begin_refuses_on_the_cursors_own_existence0x5008 on replay, the cursor undisturbed, and the top-up path: an underfunded cursor lands on exactly minimum_balance(296)
begin_admits_a_readonly_coordinate_that_the_callers_other_instruction_writesthe writability exemption, proved red against the pre-fix ELF (0x5001) before it was trusted green
real_sbf_late_token_refusal_rolls_back_fractional_position_mint_and_cursorrepaired, and now reaching the Token CPI it asserts about for the first time since 4630ad77

Conservation, stated exactly, over the whole walk:

rent_credit_after == rent_credit_before
                   + position_lamports + admission_lamports + mint_lamports
                   + cursor_lamports

with cursor_lamports == minimum_balance(296) + 4,242 — the stray the fixture plants on the address before anyone begins.

Not verified

Two seam-audit findings, and what they were actually about

tools/seam-audit reported both of this lane's frames.

LIABILITY_BASIS_MARKET_SEED_V2 spelled raw at the aggregate derivation. The tree has two dozen sites that spell this two-seed tuple by hand and the owning crate exported no constructor for it, so the honest fix was to add one rather than to copy the twenty-fifth spelling: LiabilityBasisMarketSeedsV2::new(market).as_slices() now lives beside the domain in crates/dclutch-claims-svm/src/liability_basis_state_v2.rs, refuses the zero identity where the seeds are made, and is pinned against the hand-spelled tuple by the_seeds_constructor_reproduces_the_hand_spelled_tuple_exactly. The other sites are pre-existing debt and were not touched by this lane.

PRIVILEGE_PIN_UNEXEMPTED on authenticate_lifecycle_frame, and this one was a real defect rather than a style finding. The frame pinned exact writability on every coordinate. is_writable is a transaction-level property — the runtime merges an account's privileges across every instruction of the transaction that names it — so a readonly pin does not constrain this instruction, it forbids the caller's other ones. Concretely: Finish must take the RentCredit writable and Begin only reads it, so the readonly pin at BEGIN_RENT_CREDIT made the two acts of one walk unbatchable. The same shape 16351a13 fixed on Custody, where a checkpoint pinned readonly could never compose with the Trading ingest that is its documented atomic partner.

FrameRoleV3 replaces the privilege triple and pins writability in one direction only: Written and SigningPayer must be writable, because that is a requirement of this instruction; Read and Program accept either, because it is not. Nothing is lost — a read-only coordinate arriving writable changes no byte this route reads, and every account reachable through one of its CPIs is Written or SigningPayer.

Left queued (both FRACR3's, neither in a file this lane edited)