Skip to main content

Exploit Demo: The "Shadow Withdrawal" Interleaving

This document demonstrates how Sector9’s TLA+ bridge can detect protocol-level exploits that traditional testing often misses. The "Visual Trace Replay" pieces below are product-facing mockups, not a shipped UI guarantee. For source-level checks, the same protocol should still use entry guards, regular contracts, and actor invariants.


1. The Vulnerable Code (Simplified Sector9-Style Pseudocode)

In this scenario, a Treasury actor manages user funds. It follows a "Checks-Effects-Interactions" pattern, but because it is asynchronous, it has a hidden interleaving vulnerability. The snippet is intentionally simplified to show the protocol bug; production verified Sector9 code would also need caller binding, entry_requires/requires guards where appropriate, and catch-all handling around suspending awaits.

// Treasury.sr9
persistent actor Treasury {
var balance : Nat = 1000;
var pending_payouts : Nat = 0;

// Protocol property checked by the model:
// pending_payouts <= balance

public shared({caller}) func withdraw(amount : Nat) : async () {
// 1. CHECK: BUG - ignores already pending payouts.
if (amount > balance) { return; };

// 2. INTERACT: Reserve the funds and call the ledger.
pending_payouts += amount;

try {
// SUSPENSION POINT: The environment can interleave here!
await ledger.send(caller, amount);

// 3. EFFECT: Commit the withdrawal.
balance -= amount;
pending_payouts -= amount;
} catch (e) {
pending_payouts -= amount; // Rollback reservation on failure
}
};
}

2. The TLA+ "Raw" Counter-example

When Sector9 translates this to TLA+, the model checker (TLC) explores all interleavings. It finds a state where the invariant is violated. The raw output looks like this:

Error: Invariant "pending_payouts <= balance" is violated.
Trace:
1. Action: withdraw(500) started by Alice. pending_payouts = 500, balance = 1000.
2. Action: CHECKPOINT (await ledger.send). Alice is suspended.
3. Action: withdraw(600) started by Bob.
- Evaluates: 600 > 1000? -> FALSE.
- Evaluation Error: the check ignores Alice's pending 500, so Bob is allowed to reserve another 600.
4. Action: pending_payouts becomes 1100.
5. VIOLATION: 1100 <= 1000 is FALSE.

3. The "Visual Exploit Demo" (Proposed UI)

Instead of the raw trace above, the "Visual Trace Replay" tool would overlay the exploit directly on the Sector9 source code as a step-by-step "Time-Travel" view.

Step 1: Alice's Request

Highlighted Line: pending_payouts += 500; State: balance: 1000, pending: 500 Message: Alice calls withdraw(500).

Step 2: Suspension

Highlighted Line: await ledger.send(...); UI Indicator:Alice is now suspended. State: balance: 1000, pending: 500

Step 3: The Interleaving (The Exploit)

Highlighted Line: if (amount > balance) { ... } UI Indicator: ⚠️ New Message: Bob calls withdraw(600). Logic Check: 600 > 1000 is FALSE, so Bob is incorrectly allowed to continue. TLA+ Finding: The protocol must check available capacity (balance - pending_payouts), not raw balance. Otherwise, concurrent in-flight withdrawals can reserve more than the treasury can pay.

Step 4: The Failure

Highlighted Line: pending_payouts += amount; UI Indicator:CRASH: Total committed + pending (1100) exceeds Treasury capacity (1000).


4. The Value of the Demo

For a business client (e.g., a DAO looking to store $10M), this exploit demo is useful because it ties the model-checker result back to source-level protocol behavior:

  1. Transparency: It shows exactly why the code is unsafe.
  2. Education: It teaches the developer the nuances of "Async State Drift."
  3. Assurance: Once the developer applies a fix, the model can be rerun to show that this specific counterexample is no longer reachable under the modeled assumptions.

5. The Fix (Verified)

The "Visual Demo" would then show the fix being verified:

public shared({caller}) func withdraw(amount : Nat) : async () {
// Atomic check against currently available capacity.
if (amount + pending_payouts > balance) { return; };

pending_payouts += amount;
// ... rest of the logic
}

With this fix, TLA+ can check that pending withdrawals do not exceed the balance for the modeled Alice/Bob interleavings, while the Sector9 contract lane can separately check the local update facts that support the protocol model.