Block card double-spending: delay-module + lien-aware card-collateral#14
Draft
yvesfracari wants to merge 2 commits into
Draft
Block card double-spending: delay-module + lien-aware card-collateral#14yvesfracari wants to merge 2 commits into
yvesfracari wants to merge 2 commits into
Conversation
* feat(delay): delay-module policy contract — queue, cooldown, expiration Standalone Soroban policy contract per ADR-0001: user operations queue, become executable inside [cooldown, expiration) (constructor params; defaults 180/1800 s passed at deploy, never hardcoded), and are void past the window. No admin role exists — cancel/execute are bound to the queuing user's require_auth, verified by test. All four transitions (queued, executed, cancelled, expired) emit events carrying user/kind/amount for the indexer's Spendable computation; expiry is passive, so entry_expired is emitted by a permissionless expire reaper. 10 unit tests: tight boundaries at both window edges, no-admin auth checks, amount validation, empty-window constructor rejection. * feat(delay): testnet deploy, TypeScript bindings, live queue→wait→execute demo Deployed delay-module to testnet at CAA47ICIUOVQEHZFIUJFBJYCWF6WJBLIYRRN4AJVHT5O7LNZ2LN7R72S with the ADR-0001 defaults (cooldown 180 s, expiration 1800 s) and wasm provenance metadata pointing at the commit that introduced the contract. Bindings generated into sdk/typescript/src/generated/delay-module and wired into corridorClients as delayModule. Live demo captured in docs/DEMO.md §4, all user-signed (no admin exists): queue (entry_queued) → execute during cooldown rejected with Error #4 → execute after 3 min succeeds (entry_executed), plus the cancel path (entry_cancelled) on a queued config change.
* feat(contracts): lien-aware card-collateral contract for the production vault Extends the PoC reserve/settle/release state machine with per-lock ownership and a per-user lien aggregate. get_locked_total(user) is a single O(1) read maintained at reserve, settle, and release; a breached lock contributes 0. Shortfall semantics match the PoC; lifecycle events carry the owner as a topic for per-user indexing. PoC untouched. STE-39 * feat(sdk): deploy card-collateral to testnet, generate + wire TS bindings Deployed at CA3GIFBKAKCNLNK4EMNPKUCAYKGHLGW5Y6T37SSY464DRYDFJJHE6YMK (admin recorded in deployments/testnet.json; differs from the May 30 deployer key, noted under "admins"). Lifecycle exercised live via CLI: reserve -> settle -> release with get_locked_total tracking the lien, plus a breaching settle emitting the shortfall event. STE-39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The on-chain half of the card-spend safety model. Two new Soroban contracts that together let the issuer adapter compute Spendable = on-chain balance − open liens − queued-but-unexecuted delay entries, so the same dollar can't back a card authorization and an outbound withdrawal at the same time.
delay-module— queue → cooldown → execution window (STE-40, #11)A user-initiated operation (outbound transfer or config change) never executes immediately: it is queued, becomes executable only after a cooldown (180 s on this instance), and is void after the execution window (1800 s). The cooldown is exactly the time the adapter has to observe a pending withdrawal before money can leave.
cancel/executeare bound to the queuing user'srequire_auth, andexpireis a permissionless reaper. This is a deliberate exception to the access-control pattern used by the other contracts, and it's verified by test.(id, user)topics — the events are the interface the indexer (STE-43) and issuer adapter (STE-44) compute Spendable from, not telemetry.entry_queuedcarriesexecutable_at/expires_atso liveness needs no contract read; expiry is passive (the indexer drops entries byexpires_at, never by waiting forentry_expired).ConfigChangecounts as a full-balance withdrawal (ADR-0001 decision chore(deploy): record 2026-06-05 redeployment + fail loudly on demo invoke errors #8), soqueuerejects a nonzero amount on config changes.Architecture doc:
docs/architecture/delay-module.md.card-collateral— lien-aware production vault (STE-39, #10)Extends the testnet PoC state machine so every lock has an owner and the contract maintains a per-user lien aggregate:
reserve(user, auth_id, amount)records the lien without moving tokens;settle/releasereduce it;get_locked_total(user)exposes the aggregate as a single O(1) read so the smart account and issuer adapter never enumerate per-auth locks.locked_total(user) = Σ max(0, locked − settled)over the user's open locks, maintained incrementally; a breached lock contributes 0, never a negative.shortfallevent; capping the debit is downstream adapter work). The PoC contract is untouched and stays off the audit path.ownertopic so the indexer can attribute lien changes per user.stellar-contracts =0.7.1blocks as the PoC:pausable(reservegated;settle/releasestay available to wind down) andaccess_control.Deployment & SDK
Both contracts are live on testnet (
deployments/testnet.json):delay-moduleatCAA47ICI…R72S,card-collateralatCA3GIFBK…6YMK. TypeScript bindings are generated and wired into the SDK (corridorClientsnow exposescardCollateralanddelayModule), andscripts/deploy_testnet.shdeploys both (delay params overridable viaDELAY_COOLDOWN_SECS/DELAY_EXPIRATION_SECS).docs/DEMO.mdhas live captured runs for both: queue → cooldown rejection → execute, the user-only cancel path, and the collateral reserve → settle → release lifecycle including a breaching settle.Tests
cargo test -p bleu-delay-module— full lifecycle, window boundary conditions (half-open[t+cooldown, t+expiration)), user-only cancel/execute, permissionless expire.cargo test -p bleu-card-collateral— 11 unit tests: lien accounting across mixed lock states, per-user isolation, shortfall with the lien floored at 0, double-reserve rejection, pausable circuit breaker.Out of scope (downstream tickets)
The composing smart account (STE-41), the indexer's Spendable view (STE-43), and the issuer adapter that consumes it (STE-44).