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
4.5 KiB
Task 4: Implement V1 custody accounting and adversarial unit tests
Files:
- Modify:
src/BankV1.sol - Create:
test/BankV1.t.sol - Create:
test/mocks/FeeOnTransferToken.sol - Create:
test/mocks/ReentrantToken.sol
Interfaces:
-
Produces
deposit(uint256)andwithdraw(uint256)plusDeposited,Withdrawn,ZeroAmount,InsufficientBalance, andUnexpectedAssetDelta. -
Consumes only a standard non-rebasing ERC-20 through
IERC20/SafeERC20. -
Preserves
reserves >= totalLiabilities; direct token transfers may create surplus. -
Write event and error declarations into the test expectations before production code:
event Deposited(address indexed account, uint256 amount);
event Withdrawn(address indexed account, uint256 amount);
error ZeroAmount();
error InsufficientBalance(address account, uint256 available, uint256 requested);
error UnexpectedAssetDelta(uint256 expected, uint256 actual);
-
Write failing deposit tests for exact balance/liability/reserve deltas, event emission, zero amount, paused state, inadequate allowance, inadequate wallet balance, and two independent customers. Use six-decimal constants (
1_000e6) so assertions read like the demo. -
Add
FeeOnTransferTokenwhosetransferFromdelivers99%of the requested amount. Assert a deposit reverts withUnexpectedAssetDelta(requested, received)and that the ERC-20 transfer, bank balance, and liabilities are all rolled back. -
Add
ReentrantTokenthat attempts a nestedbank.depositduringtransferFrom. Assert the nested call receivesReentrancyGuardReentrantCalland the outer call either completes once or reverts atomically according to the mock’s configured propagation mode; no double credit is permitted. -
Write failing withdrawal tests for exact deltas, event emission, zero amount, paused state, insufficient internal balance (including available/requested values), checks-effects-interactions under a transfer callback, and one account’s withdrawal leaving another account unchanged.
-
Write the surplus test: deposit 100 mUSDC, transfer 25 mUSDC directly to the proxy, assert reserves
125e6, liabilities100e6, and a normal 100 mUSDC withdrawal succeeds while the 25 mUSDC surplus remains. Assert there is no owner sweep/rescue behavior by keeping such a function out of the interface. -
Run red tests:
npm_config_offline=true forge test --match-path test/BankV1.t.sol -vvv --force
Expected red: deposit/withdraw and their errors/events are absent.
- Implement
depositwith exact received-amount validation and state credit only after a successful transfer:
function deposit(uint256 amount) external whenNotPaused nonReentrant {
if (amount == 0) revert ZeroAmount();
uint256 reservesBefore = _asset.balanceOf(address(this));
_asset.safeTransferFrom(msg.sender, address(this), amount);
uint256 reservesAfter = _asset.balanceOf(address(this));
uint256 received = reservesAfter >= reservesBefore ? reservesAfter - reservesBefore : 0;
if (received != amount) revert UnexpectedAssetDelta(amount, received);
_balances[msg.sender] += amount;
_totalLiabilities += amount;
emit Deposited(msg.sender, amount);
}
- Implement
withdrawwith checks-effects-interactions:
function withdraw(uint256 amount) external whenNotPaused nonReentrant {
if (amount == 0) revert ZeroAmount();
uint256 available = _balances[msg.sender];
if (amount > available) revert InsufficientBalance(msg.sender, available, amount);
_balances[msg.sender] = available - amount;
_totalLiabilities -= amount;
_asset.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
-
Add fuzz tests in
BankV1.t.sol: bound deposit to[1, 1_000_000e6]; bound withdrawal to[1, deposited]; prove exact reserve/liability/customer deltas and that over-withdraw always reverts. Keep a fixed seed infoundry.tomlfor presentation reproducibility while printing Foundry’s replay seed on failure. -
Run focused and aggregate green tests:
forge fmt
npm_config_offline=true forge test --match-path test/BankV1.t.sol -vvv --force
npm_config_offline=true forge test --match-path 'test/BankV1*.t.sol' --force
Expected: all V1 tests pass with validated proxy deployment enabled.
- Commit:
git add src/BankV1.sol test/BankV1.t.sol test/mocks/FeeOnTransferToken.sol test/mocks/ReentrantToken.sol
git commit -m "feat: add V1 custody accounting"