Files
uupl-smart-contract/.superpowers/sdd/2026-08-17-uups-bank-demo/task-5-brief.md
T
golemandClaude Opus 5 fa36215def docs: track superpowers working documents in git
The uups-bank-demo wave's SDD records (ledger, six task briefs and reports,
review diffs) and the brainstorm design mockups were git-ignored, so they
existed only on one sandbox VM and reached no remote — this repo had no remote
at all until now.

Removes `.superpowers/` from .gitignore and the `*` .gitignore the superpowers
plugin writes inside .superpowers/sdd/; the second blocks the directory even
with the first removed.

Excluded as ephemeral local-server state, and now ignored by name:
.last-port, .last-token (a 64-char session token for a brainstorm server on a
port that is long gone), and the per-session state/ directories.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fnwzj6McD6kSkXwjUKFKxe
2026-08-20 14:38:00 -06:00

2.9 KiB

Task 5: Prove V1 ledger conservation and solvency with stateful invariants

Files:

  • Create: test/helpers/BankHandler.sol
  • Create: test/BankInvariant.t.sol

Interfaces:

  • Consumes MockUSDC, the V1 proxy, and a bounded set of four actor addresses.

  • Produces independent ghost counters and handler accessors used by invariant assertions.

  • Later Task 11 extends the same handler with V2 transfers.

  • Write BankHandler with four immutable actor addresses and only these V1 actions:

    • deposit(uint256 actorSeed, uint256 amount): select an actor, bound amount to [1, 10_000e6], mint to the actor, approve, deposit, and increment ghostDeposited.
    • withdraw(uint256 actorSeed, uint256 amount): select an actor, return early only when its internal balance is zero, bound to [1, balance], withdraw, and increment ghostWithdrawn.
    • donate(uint256 actorSeed, uint256 amount): mint and directly transfer [1, 1_000e6] to the proxy, incrementing ghostDonated but not liabilities.

Every action uses vm.startPrank(actor)/vm.stopPrank() in a balanced scope. The handler exposes the actor array so the invariant test, not the handler, sums on-chain balances.

In invariant setup, transfer MockUSDC ownership to the handler so only the handler can mint bounded test liquidity. This avoids impersonating an owner inside actions and does not affect bank ownership.

  • Write failing invariant tests:
function invariant_liabilitiesEqualTrackedBalances() public view {
    uint256 sum;
    for (uint256 i; i < handler.actorCount(); ++i) {
        sum += bank.balanceOf(handler.actorAt(i));
    }
    assertEq(sum, bank.totalLiabilities());
}

function invariant_reservesCoverLiabilities() public view {
    assertGe(token.balanceOf(address(bank)), bank.totalLiabilities());
}

function invariant_ghostAccountingMatchesChain() public view {
    assertEq(handler.ghostDeposited() - handler.ghostWithdrawn(), bank.totalLiabilities());
    assertEq(
        handler.ghostDeposited() + handler.ghostDonated() - handler.ghostWithdrawn(),
        token.balanceOf(address(bank))
    );
}
  • Run red:
npm_config_offline=true forge test --match-path test/BankInvariant.t.sol -vvv --force

Expected red: handler implementation is absent/incomplete.

  • Implement the minimum handler. Register it with targetContract(address(handler)) and explicitly target only its three action selectors so inherited test/helper functions cannot enter the action space.

  • Run green and retain the call summary in output:

npm_config_offline=true forge test --match-path test/BankInvariant.t.sol -vvv --force

Expected: all three invariants pass for 128 runs at depth 64; the summary shows deposits, withdrawals, and donations were each exercised.

  • Commit:
git add test/helpers/BankHandler.sol test/BankInvariant.t.sol
git commit -m "test: prove V1 accounting invariants"