Surfaced during the deferred orveth spot-check of the 2026-08-11 sweep, while auditing #70. Not #70's subject — #70's examples never existed in this repo — but a real money-path coverage gap found on the way, so filing it on its own.
The gap
No test exercises a failing swap on the seller receive path. Every receive_with call site in the test module hands it a closure that returns Ok:
crates/maxplayer-core/src/payment_wallet.rs — lines 3062, 3091, 3120, 3147, 3179, 3245, 3256
.receive_with(&token, &terms, &accepted(&[MINT]), &mint(MINT), |_| async { Ok(Amount::from(1)) })
So the seller's redeem is only ever tested against a mint that says yes. The case where a seller is handed an already-spent token — a buyer replaying a token it already redeemed, or a token spent out from under us between claim and redeem — has no coverage at all on this path.
Why this is worth a test rather than a shrug
The redeem guard itself looks right: assert_redeem_mint (payment_wallet.rs:216) and CdkSellerReceive (:1510, :1854) are library code, and redeem_guard (:3228) asserts swap_calls == 0, proving the guard fires before funds move. That is good, and it is the reason this is a coverage gap rather than a suspected bug.
But "the guard fires before the swap" and "we behave correctly when the swap says the token is spent" are two different properties, and only the first is tested. The second is the one that decides whether a seller double-counts an earnings row, refuses cleanly, or wedges.
Note the asymmetry with a path that does have this covered: refuse_if_not_all_unspent (:884, used at :931 and :973) gives the saga-retire path explicit Spent/Pending fail-closed handling, with tests. The seller receive path has no equivalent exercise. The concept exists in the codebase — it just isn't applied here in test.
Suggested shape
Add at least one receive_with case whose closure returns the mint's already-spent error, and assert the seller's behaviour explicitly:
- it refuses rather than recording a receipt,
- no earnings/receipt row is written,
- the refusal is distinguishable in the log from a transport failure (a spent token is not an outage, and an operator retrying an outage is right while an operator retrying a spent token is chasing nothing).
A second case for "spent between the guard and the swap" would cover the TOCTOU shape, if the transport seam makes that expressible.
Related
#669 is currently open and replaces the hand-rolled stub with real blind signing and DLEQ at the HttpTransport seam, so the multi-mint redeem path drives cdk's real receive saga. That is plausibly the right seam to hang this test on — worth checking whether it lands first, rather than writing this against a stub that #669 is removing.
All seven stub sites and the guard anchors were verified in-tree at df223afd (v0.3.0 = origin/main tip) before filing.
Surfaced during the deferred
orvethspot-check of the 2026-08-11 sweep, while auditing #70. Not #70's subject — #70's examples never existed in this repo — but a real money-path coverage gap found on the way, so filing it on its own.The gap
No test exercises a failing swap on the seller receive path. Every
receive_withcall site in the test module hands it a closure that returnsOk:crates/maxplayer-core/src/payment_wallet.rs— lines 3062, 3091, 3120, 3147, 3179, 3245, 3256So the seller's redeem is only ever tested against a mint that says yes. The case where a seller is handed an already-spent token — a buyer replaying a token it already redeemed, or a token spent out from under us between claim and redeem — has no coverage at all on this path.
Why this is worth a test rather than a shrug
The redeem guard itself looks right:
assert_redeem_mint(payment_wallet.rs:216) andCdkSellerReceive(:1510,:1854) are library code, andredeem_guard(:3228) assertsswap_calls == 0, proving the guard fires before funds move. That is good, and it is the reason this is a coverage gap rather than a suspected bug.But "the guard fires before the swap" and "we behave correctly when the swap says the token is spent" are two different properties, and only the first is tested. The second is the one that decides whether a seller double-counts an earnings row, refuses cleanly, or wedges.
Note the asymmetry with a path that does have this covered:
refuse_if_not_all_unspent(:884, used at:931and:973) gives the saga-retire path explicit Spent/Pending fail-closed handling, with tests. The seller receive path has no equivalent exercise. The concept exists in the codebase — it just isn't applied here in test.Suggested shape
Add at least one
receive_withcase whose closure returns the mint's already-spent error, and assert the seller's behaviour explicitly:A second case for "spent between the guard and the swap" would cover the TOCTOU shape, if the transport seam makes that expressible.
Related
#669 is currently open and replaces the hand-rolled stub with real blind signing and DLEQ at the
HttpTransportseam, so the multi-mint redeem path drives cdk's real receive saga. That is plausibly the right seam to hang this test on — worth checking whether it lands first, rather than writing this against a stub that #669 is removing.All seven stub sites and the guard anchors were verified in-tree at
df223afd(v0.3.0 =origin/maintip) before filing.