feat(stellar): production-harden stealth-vault (admin, pause, Kani proofs, benches, audit doc) - #173
Merged
truthixify merged 1 commit intoAug 26, 2026
Conversation
Bring stealth-vault up to the posture stealth-sender and wraith-names already have: an admin, a pause switch, machine-checked time-lock invariants, bench coverage, and an audit write-up. - init(admin, announcer) records a pause admin and seeds the grace period; pause/unpause guard deposit only, so claim, refund, and the new permissionless refund stay callable during an incident. - refund_permissionless(caller, deposit_id) returns a deposit to its depositor one grace period after refund_after, so a lost depositor key cannot strand funds. It emits the same refund event and refund_count metric as the depositor path. - The grace period is stored and admin-retunable via set_grace_period instead of being a hard-coded constant; reads fall back to DEFAULT_GRACE_PERIOD for already-deployed vaults. - Fix deposit announcing under scheme_id 1 while stealth-announcer asserts on 2, which would have reverted every deposit against the production announcer. tests/announcer.rs now wires the real announcer so the two cannot drift. - Fix the refund-window check wrapping under the release profile's overflow-checks = false; both window computations now saturate. - Add three Kani proofs (claim before unlock errors, refund before refund_after errors, claim and refund mutually exclusive) plus a saturating-arithmetic anchor, verified against the real contract bodies via a cfg(kani) mock_sdk, and wire stealth-vault into the existing stellar-kani CI job. - Add deposit/claim/refund/refund_permissionless gas benches. - Document the deposit-id derivation and the single-invocation Soroban model, including an explicit "no reentrancy guard required" section, in a new AUDIT_SUMMARY.md linked from stellar/README.md; record the vault's rows in PAUSE.md, METRICS.md, and MIGRATION_V0_TO_V1.md. Closes wraith-protocol#156
|
@DSOTec Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Contributor
|
Merged. Pause guards with claim and refund still callable while paused, permissionless refund after grace, and a snapshot per path. The mock_sdk split keeps the proofs readable too. Strong work @DSOTec, second substantial contracts PR from you this wave. |
4 tasks
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.
Problem
stellar/stealth-vaultshipped in Wave 6 as a time-locked deposit primitive butnever got the hardening pass that
stealth-senderandwraith-namesreceived.It is the last core contract that custodies user funds and it had:
AUDIT_SUMMARY.md;GRACE_PERIOD = 1000hard-coded, changeable only by redeploying;Two live defects surfaced while wiring the benches and the real announcer:
depositannounced underscheme_id = 1, butstealth-announcerasserts
scheme_id == STELLAR_V2_SCHEME_ID(2). Every deposit against theproduction announcer would have panicked. The unit tests registered a
permissive mock announcer, so nothing caught it.
refund_after <= unlock_ledger + GRACE_PERIODis a wrapping add under the workspace release profile (
overflow-checks = false),so an
unlock_ledgernearu32::MAXadmitted a window the validation wasmeant to reject.
Solution
Admin + pause (
src/lib.rs)init(admin, announcer)records a pause admin and seeds the grace period.pause/unpause/is_paused/admin, mirroring the sender's and names'admin-panic pattern so the posture is uniform across contracts.
depositis guarded.claim,refund, andrefund_permissionlessstaycallable while paused — parity with the sender's
withdraw_manyexception, soan admin can never trap user funds.
Permissionless refund
refund_permissionless(caller, deposit_id)opens one grace period afterrefund_after. Funds always route to the recorded depositor;calleronlypays the fee and is authorised so the invocation is attributable. It emits the
same
refundevent andrefund_countmetric as the depositor path, soindexers need no change.
Configurable grace period
initfromDEFAULT_GRACE_PERIOD, retunable by the admin viaset_grace_period(rejects zero). Reads fall back to the default, so vaultsdeployed before the key existed keep working.
Defect fixes
ANNOUNCE_SCHEME_ID = 2, withtests/announcer.rswiring the realstealth-announcerso the two cannot drift again.saturating_add.Kani proofs (
src/proofs/mod.rs,src/mock_sdk.rs)proof_claim_before_unlock_always_errorsproof_refund_before_refund_after_always_errorsproof_claim_and_refund_are_mutually_exclusiveproof_permissionless_window_never_precedes_refund_after, anchoring thesaturating arithmetic (b) leans on.
They run against the real
claim/refund/refund_permissionlessbodies, compiled against a hand-rolled
mock_sdkin place ofsoroban-sdk—the same
cfg(kani)patternstealth-registryalready uses. The harness iszero-sized and heap-free (state in one
static, fixed-capacity slot arrays) soCBMC stays well inside the CI budget.
stellar/stealth-vaultis added to theexisting
stellar-kanijob.Metrics —
deposit_count,deposit_volume,claim_count,refund_countwere wired in #171; this PR adds the permissionless path's
refund_countandpins all four in
stellar/METRICS.md's per-contract table.Docs — new
stellar/stealth-vault/AUDIT_SUMMARY.mdmirroring the sender's,covering the deposit-id derivation (including the ephemeral-key-reuse collision
case) and an explicit no reentrancy guard required section grounded in the
single-invocation Soroban model. Linked from
stellar/README.md.Testing
New coverage: admin/init (5), pause (6), permissionless refund (4), metric shape
for the permissionless path (1), real-announcer integration (2).
The four new bench rows are new ops, so
bench/compare.pyreports them under"new ops (no baseline gate)" and the +5% PR gate is unaffected.
PERF.md'sauto-managed block was regenerated with
bench/update_perf_md.py; CI refreshesit again on the next
developpush.Notes for reviewers
initgains anadminparameter — an ABI break. The vault is not indeploy.sh,contract-ids.json, orabi/, so nothing in-repo depends on theold signature.
refund's signature is unchanged; the permissionless path is a newentrypoint rather than a change to the existing one.
set_grace_periodmoves the permissionless window for deposits already inflight (it is computed at call time). That is deliberate — it is the lever an
admin needs — and proof (b) shows it can never bring the window forward past
refund_after.Closes #156