Files
uupl-smart-contract/.superpowers/sdd/2026-08-17-uups-bank-demo/task-4-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

4.5 KiB
Raw Blame History

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) and withdraw(uint256) plus Deposited, Withdrawn, ZeroAmount, InsufficientBalance, and UnexpectedAssetDelta.

  • 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 FeeOnTransferToken whose transferFrom delivers 99% of the requested amount. Assert a deposit reverts with UnexpectedAssetDelta(requested, received) and that the ERC-20 transfer, bank balance, and liabilities are all rolled back.

  • Add ReentrantToken that attempts a nested bank.deposit during transferFrom. Assert the nested call receives ReentrancyGuardReentrantCall and the outer call either completes once or reverts atomically according to the mocks 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 accounts withdrawal leaving another account unchanged.

  • Write the surplus test: deposit 100 mUSDC, transfer 25 mUSDC directly to the proxy, assert reserves 125e6, liabilities 100e6, 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 deposit with 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 withdraw with 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 in foundry.toml for presentation reproducibility while printing Foundrys 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"