Skip to content

feat: implement v2 credit withdrawal and reentrancy guard - #88

Merged
collinsezedike merged 4 commits into
mainfrom
feat/v2-credit-withdrawal
Aug 12, 2026
Merged

feat: implement v2 credit withdrawal and reentrancy guard#88
collinsezedike merged 4 commits into
mainfrom
feat/v2-credit-withdrawal

Conversation

@collinsezedike

Copy link
Copy Markdown
Collaborator

Summary

Implements #70: credit withdrawal and its reentrancy guard.

  • New withdraw(owner, id, destination): transfers owner's entire withdrawable Credit(id, owner) balance to destination. Requires owner's authorization. destination can be any address, not necessarily owner itself, so a token that rejects transfers to owner directly can't permanently strand funds there.
  • Effects before interactions: the credit balance is zeroed, and Resolution's new outstanding_liability/withdrawn_total fields updated, before the outgoing transfer. If the transfer fails, the whole call fails and every write above rolls back with it (Soroban reverts all of a failed invocation's storage writes, the same guarantee every other value-moving function here already relies on): the credit is never consumed by a failed transfer.
  • settle now also maintains Resolution.outstanding_liability, incrementing it by whatever it accrues (a position's own payout, and any dust) in the same call that persists settled_recipient_weight/settled_reward_total, so there's a single source of truth for what's owed but not yet withdrawn per dispute.
  • New contract-wide reentrancy guard (enter_reentrancy_guard/exit_reentrancy_guard/check_reentrancy_guard), addressing the issue's second ask (which also applies to registration from [Feature] V2: implement dispute and third-party registration #66, called out there as requested): held for the duration of every external token transfer this contract initiates (assert_outcome, dispute, register, finalize, withdraw), and checked at the entry of reveal, resolve_outcome, and settle as well, even though none of those three move tokens themselves. A non-standard token whose transfer implementation calls back into this contract mid-transfer, instead of a well-behaved SEP-41 token that just updates balances, would otherwise be able to act on state that looks complete (because it's written before the transfer, the existing state-before-external-call pattern) while the tokens backing it haven't actually moved yet.

Test plan

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test pass locally
  • cargo build -p tholos --target wasm32v1-none --release and cargo build --workspace --target wasm32v1-none --release pass locally
  • mdbook build docs passes locally
  • CONTRACT.md updated if the public interface changed — not applicable, tholos-v2 isn't documented there yet (tracked separately)
  • scripts/testnet-smoke.sh run against testnet — not run; this only touches tholos-v2, which isn't deployed yet
  • What you manually verified: added tests for withdrawal (balance transfer, custom destination, no-credit and double-withdraw failures, and a full settle-then-withdraw scenario across every participant confirming the contract's own token balance reaches exactly 0 and outstanding_liability/withdrawn_total end up consistent). The reentrancy guard is tested by directly setting the guard flag in storage (via env.as_contract) to simulate a stuck lock, since triggering real reentrancy would need a separate hostile-token contract, and confirming every guarded entrypoint rejects with ReentrancyGuardActive while held, then succeeds once released.

@collinsezedike
collinsezedike merged commit 4d9ba5e into main Aug 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant