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
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
BankHandlerwith 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 incrementghostDeposited.withdraw(uint256 actorSeed, uint256 amount): select an actor, return early only when its internal balance is zero, bound to[1, balance], withdraw, and incrementghostWithdrawn.donate(uint256 actorSeed, uint256 amount): mint and directly transfer[1, 1_000e6]to the proxy, incrementingghostDonatedbut 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"