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
9.1 KiB
Task 5 Report: V1 Stateful Accounting Invariants
Status
Complete. Commit 6dcbb03 (test: prove V1 accounting invariants) adds only the two task-listed test files. Production contracts were not modified.
Handler design
BankHandlerowns four fixed immutable actor addresses (0x1001through0x1004) and exposes the bounded actor set throughactorCount()andactorAt().depositselects an actor from the seed, bounds the amount to[1, 10_000e6], mints test liquidity, performs actor approval/deposit inside a balanced prank scope, and incrementsghostDepositedonly after success.withdrawselects an actor, returns early only for a zero bank balance, bounds the amount to[1, balance], performs the withdrawal inside a balanced prank scope, and incrementsghostWithdrawnonly after success.donateselects an actor, bounds the amount to[1, 1_000e6], mints test liquidity, directly transfers it to the proxy inside a balanced prank scope, and incrementsghostDonatedwithout touching liabilities.- In setup,
MockUSDCownership transfers from the token owner to the handler. Bank ownership remains unchanged. - The handler is registered with
targetContract(address(handler)), andtargetSelectorrestricts generated calls to exactlydeposit,withdraw, anddonate.
Invariant derivations
- Ledger conservation: the test independently enumerates the four actors and sums
bank.balanceOf(actor). That sum must equalbank.totalLiabilities(). - Solvency: the token's actual proxy balance must be greater than or equal to total liabilities. Direct donations create only surplus.
- Independent flow accounting:
ghostDeposited - ghostWithdrawn == totalLiabilitiesbecause only customer custody flows create or extinguish liabilities.ghostDeposited + ghostDonated - ghostWithdrawn == proxy token reservesbecause donations increase reserves without increasing liabilities.
Ghost counters record successful handler inputs, while expected chain state is read from the real proxy and token. The handler does not mirror per-account bank balances, and the invariant test—not the handler—sums on-chain actor balances.
Changed files
test/helpers/BankHandler.sol: bounded V1 stateful action handler and independent ghost counters.test/BankInvariant.t.sol: handler setup, selector targeting, and three conservation/solvency invariants.
TDD evidence
PATH was set in a separate persistent shell step:
export PATH=/tmp/codius-node-v24.18.0/bin:/home/golem/.foundry/bin:$PATH
RED
Exact command:
npm_config_offline=true forge test --match-path test/BankInvariant.t.sol -vvv --force
Output before either requested file existed:
[⠒] Compiling 28 files with Solc 0.8.35
Compiler run successful!
No tests found in project! Forge looks for functions that start with `test`
This was the expected feature-absent failure: no invariant target or handler existed.
GREEN
Exact command:
npm_config_offline=true forge test --match-path test/BankInvariant.t.sol -vvv --force
Key verbatim output:
Ran 3 tests for test/BankInvariant.t.sol:BankInvariantTest
[PASS] invariant_ghostAccountingMatchesChain() (runs: 128, calls: 8192, reverts: 0)
[PASS] invariant_liabilitiesEqualTrackedBalances() (runs: 128, calls: 8192, reverts: 0)
[PASS] invariant_reservesCoverLiabilities() (runs: 128, calls: 8192, reverts: 0)
| Contract | Selector | Calls | Reverts | Discards |
| BankHandler | deposit | 2666 | 0 | 0 |
| BankHandler | donate | 2791 | 0 | 0 |
| BankHandler | withdraw | 2735 | 0 | 0 |
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 3.43s (4.86s CPU time)
Ran 1 test suite in 3.43s (3.43s CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)
The configured 128 runs and depth 64 produced 8,192 calls per invariant. The fixed seed yielded the same selector counts in the later full-suite run, proving all three actions were exercised.
Formatting and full-suite validation
Formatting check after applying forge fmt:
forge fmt --check test/helpers/BankHandler.sol test/BankInvariant.t.sol
Output: empty, exit success.
Full relevant suite command:
npm_config_offline=true forge test -vvv --force
Result:
Ran 4 test suites in 6.23s (12.11s CPU time): 43 tests passed, 0 failed, 0 skipped (43 total tests)
The invariant portion again passed all three properties for 128 runs and 8,192 calls each, with 2,666 deposits, 2,791 donations, 2,735 withdrawals, and zero reverts/discards. Compilation emitted only two pre-existing OpenZeppelin Foundry Upgrades mutability warnings in StringFinder.sol and DefenderDeploy.sol.
Mutation and self-review
Mentally checked realistic mutations:
- Omitting or mis-sizing a deposit/withdraw liability update breaks the ghost-liability equality.
- Updating aggregate liabilities without the selected account balance (or vice versa) breaks the summed-ledger equality.
- Failing to transfer reserves on deposit/withdraw breaks reserve ghost accounting and may break solvency.
- Treating donations as liabilities breaks both the ghost-liability equation and the intended surplus model.
- Draining or under-crediting proxy reserves breaks exact reserve ghost accounting and the solvency inequality.
git diff --check and the final formatting check passed. Pre-commit status contained only the two task-listed files. The commit contains exactly those files and 122 inserted test lines. No production files changed.
Concerns
None. The only warnings are existing third-party mutability warnings noted above.
Fix Round 1: Genuine handler-absent RED evidence
The original RED evidence above is superseded by this fix round. A missing invariant test only established that Forge had no matching tests; it did not establish that the invariant specification failed when the handler feature was absent.
Covering test state
An isolated temporary project was created at /tmp/task-5-red.dLa7CJ. It contained the committed test/BankInvariant.t.sol unchanged but deliberately excluded test/helpers/BankHandler.sol. The real worktree remained at commit 6dcbb03f3c79979416c2f0dd37556539276a4e29, with its tracked files untouched.
Exact isolation command:
rsync -a --exclude=.git --exclude=out --exclude=cache --exclude=test/helpers/BankHandler.sol ./ /tmp/task-5-red.dLa7CJ/
Precondition checks confirmed:
isolated invariant test: present
isolated handler: absent
PATH was set separately in the isolated shell:
export PATH=/tmp/codius-node-v24.18.0/bin:/home/golem/.foundry/bin:$PATH
Genuine RED
The brief's command was then run literally in the isolated project:
npm_config_offline=true forge test --match-path test/BankInvariant.t.sol -vvv --force
Relevant verbatim output:
Compiler run failed:
Error (6275): Source "test/helpers/BankHandler.sol" not found: File not found. Searched the following locations: "/tmp/task-5-red.dLa7CJ".
ParserError: Source "test/helpers/BankHandler.sol" not found: File not found. Searched the following locations: "/tmp/task-5-red.dLa7CJ".
--> test/BankInvariant.t.sol:5:1:
|
5 | import {BankHandler} from "./helpers/BankHandler.sol";
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: Compilation failed
Exit status was captured immediately afterward with echo $?:
1
This is a genuine RED because the completed invariant test is present and compilation fails specifically at its dependency on the absent stateful handler. Adding BankHandler.sol is the production-of-the-test-suite change that makes this exact specification compile and run.
Fresh GREEN in the real worktree
PATH was again set separately in a new shell rooted at the real worktree:
export PATH=/tmp/codius-node-v24.18.0/bin:/home/golem/.foundry/bin:$PATH
The brief's GREEN command was run literally:
npm_config_offline=true forge test --match-path test/BankInvariant.t.sol -vvv --force
Relevant verbatim output, repeated for each of the three invariants:
Ran 3 tests for test/BankInvariant.t.sol:BankInvariantTest
[PASS] invariant_ghostAccountingMatchesChain() (runs: 128, calls: 8192, reverts: 0)
| Contract | Selector | Calls | Reverts | Discards |
| BankHandler | deposit | 2666 | 0 | 0 |
| BankHandler | donate | 2791 | 0 | 0 |
| BankHandler | withdraw | 2735 | 0 | 0 |
[PASS] invariant_liabilitiesEqualTrackedBalances() (runs: 128, calls: 8192, reverts: 0)
[PASS] invariant_reservesCoverLiabilities() (runs: 128, calls: 8192, reverts: 0)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 3.53s (4.76s CPU time)
Ran 1 test suite in 3.53s (3.53s CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)
Exit status captured immediately with echo $?:
0
All three selectors were exercised over 128 runs at depth 64 (8,192 calls per invariant), with no revert or discard. No tracked commit was needed for Fix Round 1 because the implementation was already correct; only the ignored evidence report changed. The deferred actorAt minor was not addressed.