# UUPS Bank Demo Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Deliver a deterministic, local-first UUPS “bank” demo that starts at a verified V1 checkpoint, adds internal balance transfers in V2 without losing state, and explains the result through a read-only React operations console. **Architecture:** Foundry scripts are the only state-changing control plane. A six-decimal `MockUSDC` and a UUPS `BankV1` implementation run behind a stable ERC-1967 proxy on Anvil or optional Base Sepolia. Scripts export a public deployment manifest and contract ABIs; a wagmi/viem client reads those artifacts and renders reserves, liabilities, identities, actors, and events without a signer or wallet connector. V2 inherits V1, adds no storage, and adds one internal transfer function. **Tech Stack:** Foundry `v1.7.1`, forge-std `v1.16.1`, Solidity `0.8.35`, OpenZeppelin Contracts Upgradeable `v5.6.1`, OpenZeppelin Foundry Upgrades `v0.4.1`, OpenZeppelin Upgrades Core `1.46.0`, Node `24.18.0`, npm `11.17.0`, React `19.2.8`, TypeScript compiler `7.0.2` via `@typescript/native`, TypeScript API compatibility `6.0.2` via the `typescript` alias, Vite `8.2.0`, wagmi `3.7.5`, viem `2.55.8`, TanStack Query `5.101.4`, Vitest `4.1.10`, Testing Library React `16.3.2`, and jsdom `30.0.1`. ## Global Constraints - The contracts are educational and unaudited. Every user-facing surface says: `Educational demo — mock token — never use real funds.` - State-changing scripts accept only chain IDs `31337` and `84532`; no mainnet RPC, address, target, or configuration is added. - Local accounts come only from Anvil’s standard development mnemonic. Testnet signing uses a named encrypted Foundry keystore and `--account`; no supported command accepts a raw private key or mnemonic environment variable. - Every application call uses the proxy address. The implementation address is read only for validation and explanation. - `Upgrades`, never `UnsafeUpgrades`, performs deploy/upgrade validation. The sole validator allowance is `@custom:oz-upgrades-unsafe-allow constructor` immediately above the constructor that calls `_disableInitializers()`; no Options `unsafeAllow`, exclude, skip, `UnsafeUpgrades`, reachable annotation, or other bypass is permitted. BankV1 uses OpenZeppelin 5.6.1 `ReentrancyGuardTransient`, the user-approved constructor-free guard for this project's Cancun-targeted Anvil and Base Sepolia networks. - V2 does not add, delete, reorder, or change the type of any storage variable and has no initializer. - The browser has no connector, signer, transaction client, or write button. Failed reads remain unknown; they are never rendered as zero. - Generated Foundry output, local manifests, copied web artifacts, `.demo/` process state, `.env`, and `.superpowers/` are ignored by Git. - Run each red/green command exactly as written. A test that unexpectedly passes in a red step means the test is not proving the intended behavior; fix the test before implementation. - Use exact versions and committed lockfiles/submodule revisions. Do not replace exact versions with ranges. - OpenZeppelin Foundry Upgrades `v0.4.1` invokes `npx @openzeppelin/upgrades-core@^1.45.0`; the root lockfile pins the satisfying implementation to `1.46.0`, and every validation-bearing command runs with `npm_config_offline=true` after setup proves the local CLI is available. - Commit only files listed for the task and inspect `git status --short` before every commit so unrelated user changes remain untouched. ## File and Interface Map | Area | Files | Produces / consumes | | --- | --- | --- | | Toolchain | `.nvmrc`, `.env.example`, `.gitignore`, `.gitmodules`, `foundry.lock`, `foundry.toml`, `remappings.txt`, root/web `package.json` and `package-lock.json`, `Makefile` | Pins compilers, validation CLI, and dependencies; defines the command contract used by scripts, CI-style verification, and docs | | V1 contracts | `src/MockUSDC.sol`, `src/BankV1.sol` | Produces token and bank ABIs; consumes OpenZeppelin ERC-20, access, pause, reentrancy, initializer, and UUPS modules | | V2 contract | `src/BankV2.sol` | Consumes V1 storage and behavior; produces `transferBalance`, `BalanceTransferred`, and version `2` | | Test support | `test/helpers/BankTestBase.sol`, `test/mocks/FeeOnTransferToken.sol`, `test/mocks/ReentrantToken.sol`, `test/mocks/IncompatibleBank.sol`, `test/mocks/NonUUPSImplementation.sol`, `test/helpers/BankHandler.sol`, `test/helpers/BankV2Handler.sol` | Reusable actors, proxy deployment, hostile token behavior, invalid upgrades, and stateful action generation | | Contract tests | `test/MockUSDC.t.sol`, `test/BankV1Admin.t.sol`, `test/BankV1.t.sol`, `test/BankInvariant.t.sol`, `test/BankV2.t.sol`, `test/BankUpgrade.t.sol` | Consumes contracts and Upgrades plugin; proves units, fuzz properties, invariants, authorization, and upgrade continuity | | Foundry scripts | `script/lib/DemoScript.sol`, `script/DeployV1.s.sol`, `script/SeedV1Demo.s.sol`, `script/CheckState.s.sol`, `script/UpgradeV2.s.sol`, `script/TransferV2Demo.s.sol` | Consumes RPC plus explicit sender/account; produces transactions, postcondition evidence, and `deployments/active.json` | | Artifact/manifest bridge | `tools/sync-web-artifacts.mjs`, `tools/finalize-manifest.mjs`, `tools/select-manifest.mjs`, `tools/publish-web-manifest.mjs`, `web/src/generated/.gitkeep`, `web/public/.gitkeep` | Generates ABIs without a chain; confirms receipts into per-chain manifests; explicitly selects/publishes the active network | | Web data | `web/src/types/dashboard.ts`, `web/src/config/chains.ts`, `web/src/config/manifest.ts`, `web/src/data/bankClient.ts` | Consumes manifest, ABI, public RPC; produces validated immutable dashboard snapshots and decoded activity | | Web UI | `web/src/main.tsx`, `web/src/App.tsx`, `web/src/app.css`, `web/src/hooks/useBankDashboard.ts`, `web/src/components/*.tsx` | Consumes dashboard states; renders a strictly read-only operations console | | Web tests | `web/src/test/setup.ts`, `web/src/**/*.test.ts`, `web/src/**/*.test.tsx` | Proves schema, reads, decoding, accounting, ready/loading/stale/disconnected/mismatch views, and read-only behavior | | Orchestration | `tools/doctor.sh`, `tools/process-lib.sh`, `tools/demo-local.sh`, `tools/reset-local.sh`, `tools/test-process-safety.sh`, `Makefile` | Produces memorable commands and project-scoped Anvil/Vite lifecycle; consumes only `.demo/` PID files | | Learning/demo docs | `README.md`, `docs/LEARNING_GUIDE.md`, `docs/PRESENTER_RUNBOOK.md` | Consumes actual commands and exact Act 1/Act 3 states; produces quick start, explanations, live script, and recovery steps | The public manifest contract is: ```json { "schemaVersion": 1, "network": "anvil", "chainId": 31337, "deploymentBlock": 1, "rpcUrl": "http://127.0.0.1:8545", "explorerBaseUrl": null, "token": "0x...", "proxy": "0x...", "implementation": "0x...", "owner": "0x...", "actors": [ { "label": "Alice", "address": "0x..." }, { "label": "Bob", "address": "0x..." } ] } ``` `rpcUrl` is public local/testnet configuration, not a credential-bearing vendor URL. Testnet operators provide `BASE_SEPOLIA_RPC_URL` at runtime; the script writes only a redacted/public URL selected explicitly for display, or omits it and the browser requires `VITE_RPC_URL`. Confirmed manifests are stored separately as ignored `deployments/anvil.json` and `deployments/base-sepolia.json`. `deployments/active.json` is an atomic copy selected from one of those files for scripts/the browser. Local reset may remove only the Anvil manifest and an active/copy whose parsed chain ID is `31337`; it must preserve Base Sepolia state. Explicit `make select-anvil` and `make select-base-sepolia` targets switch the active copy without overwriting the other network. --- ### Task 1: Pin and prove the repository toolchain **Files:** - Modify: `.gitignore` - Create: `.nvmrc` - Create: `.env.example` - Create: `.gitmodules` (generated by `forge install`) - Create: `foundry.lock` (generated by `forge install`) - Create: `foundry.toml` - Create: `remappings.txt` - Create: `Makefile` - Create: `package.json` - Create: `package-lock.json` (generated by npm) - Create: `tools/check-upgrades-cli.mjs` - Create: `web/package.json` - Create: `web/package-lock.json` (generated by npm) - Create: `web/index.html` - Create: `web/tsconfig.json` - Create: `web/tsconfig.app.json` - Create: `web/tsconfig.node.json` - Create: `web/vite.config.ts` - Create: `web/eslint.config.js` - Create: `web/src/test/toolchain.test.ts` **Interfaces:** - Produces `make doctor`, `make setup`, and the initial verification gate that later tasks extend. - Produces exact dependency revisions for every later Solidity and web task. - Consumes no application source. - [ ] Verify host tools and record the expected initial limitation: ```bash forge --version anvil --version node --version npm --version make --version ``` Expected now: Foundry commands may be missing; Node/npm/make print their installed versions. If Foundry is missing during execution, request permission to install Foundry from its official installer, then pin with `foundryup -i 1.7.1`. Do not silently install system-wide tools. - [ ] Extend `.gitignore` with exactly these runtime classes: ```gitignore .env .env.local .demo/ cache/ out/ broadcast/ deployments/*.json deployments/**/*.json !deployments/*.example.json node_modules/ web/node_modules/ web/dist/ web/coverage/ web/public/deployment.json web/src/generated/*.ts !.gitkeep .superpowers/ ``` - [ ] Pin Node/npm in `.nvmrc` and create a private root `package.json` with the same `packageManager`/`engines` fields plus exactly one dev dependency: `"@openzeppelin/upgrades-core": "1.46.0"`. Generate and commit the root lockfile with `npm install --save-exact`. This locally satisfies the plugin’s hard-coded `^1.45.0` range. - [ ] Pin the browser dependencies in `web/package.json`; the package must be private and use only exact versions: ```json { "name": "uups-bank-operations-console", "private": true, "version": "0.1.0", "type": "module", "packageManager": "npm@11.17.0", "engines": { "node": ">=24.18.0 <25", "npm": ">=11.17.0 <12" }, "scripts": { "dev": "vite --host 127.0.0.1", "lint": "eslint . --max-warnings 0", "typecheck": "tsc -b --pretty false", "test": "vitest run", "build": "tsc -b && vite build" } } ``` Add exact runtime dependencies `@tanstack/react-query@5.101.4`, `react@19.2.8`, `react-dom@19.2.8`, `viem@2.55.8`, and `wagmi@3.7.5`. Add exact dev dependencies `@eslint/js@10.0.1`, `@testing-library/dom@10.4.1`, `@testing-library/react@16.3.2`, `@types/node@24.10.0`, `@types/react@19.2.14`, `@types/react-dom@19.2.4`, `@typescript/native@npm:typescript@7.0.2`, `@vitejs/plugin-react@6.0.4`, `eslint@10.0.1`, `eslint-plugin-react-hooks@7.1.1`, `eslint-plugin-react-refresh@0.5.3`, `globals@17.7.0`, `jsdom@30.0.1`, `typescript@npm:@typescript/typescript6@6.0.2`, `typescript-eslint@8.65.0`, `vite@8.2.0`, and `vitest@4.1.10`. The TypeScript 7 native compiler ships without the API consumed by typescript-eslint, so install its range-free TypeScript 6 compatibility alias under `typescript` and the range-free TypeScript 7 compiler alias under `@typescript/native`. If npm rejects one exact revision because the registry changed, verify the official release before changing both this plan and the package file. - [ ] Configure Foundry in `foundry.toml`: ```toml [profile.default] src = "src" test = "test" script = "script" out = "out" libs = ["lib"] solc_version = "0.8.35" evm_version = "cancun" optimizer = true optimizer_runs = 200 ffi = true ast = true build_info = true extra_output = ["storageLayout"] fs_permissions = [ { access = "read", path = "out" }, { access = "read-write", path = "deployments" } ] [fuzz] runs = 512 seed = "0x5555505342414e4b" [invariant] runs = 128 depth = 64 fail_on_revert = true ``` - [ ] Install exact Solidity dependencies as Git submodules and write canonical remappings: ```bash forge install foundry-rs/forge-std@v1.16.1 forge install OpenZeppelin/openzeppelin-foundry-upgrades@v0.4.1 forge install OpenZeppelin/openzeppelin-contracts-upgradeable@v5.6.1 git submodule update --init --recursive ``` ```text forge-std/=lib/forge-std/src/ openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/ @openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ ``` Do not install a second top-level copy of `openzeppelin-contracts`; the upgradeable submodule’s pinned transitive copy supplies both canonical remappings. - [ ] Create `tools/check-upgrades-cli.mjs`. It must assert that the plugin source contains `UPGRADES_CORE = "^1.45.0"`, the root lockfile resolves `@openzeppelin/upgrades-core` to exactly `1.46.0`, and the locally installed package reports `1.46.0`. Then prove the CLI can start with networking disabled: ```bash npm ci node tools/check-upgrades-cli.mjs npm_config_offline=true npx @openzeppelin/upgrades-core@^1.45.0 validate --help ``` Expected: all commands exit `0` without fetching. Do not patch the vendored OpenZeppelin version constant. - [ ] Create Vite/React/TypeScript/ESLint/Vitest configuration manually so no unpinned scaffold generator is executed. Configure jsdom, `web/src/test/setup.ts`, strict TypeScript, and React refresh. Run: ```bash npm --prefix web install --save-exact npm --prefix web test ``` Expected red: `toolchain.test.ts` cannot import the not-yet-created `src/config/toolchain.ts`. - [ ] Create `web/src/config/toolchain.ts` exporting the display labels `Foundry 1.7.1`, `Solidity 0.8.35`, `OpenZeppelin 5.6.1`, and `UUPS`; make the test assert those exact values. - [ ] Add an initial `Makefile` with shell safety (`SHELL := /bin/bash`, `.SHELLFLAGS := -euo pipefail -c`) and non-destructive targets: ```make .PHONY: doctor setup verify doctor: @./tools/doctor.sh setup: @git submodule update --init --recursive @npm ci @npm --prefix web ci verify: @forge fmt --check @forge clean @npm_config_offline=true forge build --force @npm_config_offline=true forge test --force @npm --prefix web run lint @npm --prefix web run typecheck @npm --prefix web test @npm --prefix web run build ``` Create a temporary minimal `tools/doctor.sh` that reports missing commands and exact expected versions without installing anything; Task 9 replaces it with full port/config checks. - [ ] Run the green foundation checks: ```bash forge fmt --check forge clean npm_config_offline=true forge build --force npm_config_offline=true forge test --force node tools/check-upgrades-cli.mjs npm --prefix web run lint npm --prefix web run typecheck npm --prefix web test npm --prefix web run build ``` Expected: all exit `0`; the empty Solidity source tree builds and the toolchain test passes. - [ ] Commit the foundation: ```bash git add .gitignore .nvmrc .env.example .gitmodules foundry.lock foundry.toml remappings.txt package.json package-lock.json Makefile tools/check-upgrades-cli.mjs tools/doctor.sh lib web git commit -m "build: pin demo toolchains" ``` --- ### Task 2: Build the valueless six-decimal mock asset with TDD **Files:** - Create: `src/MockUSDC.sol` - Create: `test/MockUSDC.t.sol` **Interfaces:** - Produces `MockUSDC.mint(address,uint256)`, `decimals() == 6`, and standard ERC-20 behavior. - Consumed by the bank tests, deployment scripts, and dashboard ABI export. - [ ] Write `test/MockUSDC.t.sol` first. Cover the exact name/symbol/decimals, owner-only mint, successful mint, transfer/approve behavior inherited from ERC-20, and zero initial supply. Use `vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, stranger))` for non-owner minting. - [ ] Run the focused test and observe red: ```bash forge test --match-path test/MockUSDC.t.sol -vvv --force ``` Expected red: `src/MockUSDC.sol` is missing. - [ ] Implement only the tested contract: ```solidity /// @notice Educational mock token with no monetary value. Never use as real USDC. contract MockUSDC is ERC20, Ownable { constructor(address initialOwner) ERC20("Mock USD Coin", "mUSDC") Ownable(initialOwner) {} function decimals() public pure override returns (uint8) { return 6; } function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } } ``` Reject a zero `initialOwner` through OpenZeppelin’s `OwnableInvalidOwner` behavior. Do not add faucet, burn, permit, blacklist, proxy, or bank-specific logic. - [ ] Run red-to-green verification: ```bash forge fmt forge test --match-path test/MockUSDC.t.sol -vvv --force ``` Expected: all mock-token tests pass. - [ ] Commit: ```bash git add src/MockUSDC.sol test/MockUSDC.t.sol git commit -m "feat: add valueless mock USDC" ``` --- ### Task 3: Establish the V1 proxy, initialization, views, and administration **Files:** - Create: `src/BankV1.sol` - Create: `test/helpers/BankTestBase.sol` - Create: `test/BankV1Admin.t.sol` **Interfaces:** - Produces `initialize(address,address)`, `asset()`, `balanceOf(address)`, `totalLiabilities()`, `contractVersion()`, `pause()`, `unpause()`, and owner-authorized UUPS upgrades. - Consumes `MockUSDC` and OpenZeppelin `Upgrades.deployUUPSProxy`. - Freezes V1 application storage as `_asset`, `_balances`, `_totalLiabilities`, then `uint256[47] __gap`. - [ ] Create `BankTestBase.sol` with deterministic `owner`, `alice`, `bob`, and `stranger` addresses from `makeAddr`; deploy `MockUSDC`; deploy the proxy with: ```solidity proxy = Upgrades.deployUUPSProxy( "BankV1.sol:BankV1", abi.encodeCall(BankV1.initialize, (address(token), owner)) ); bank = BankV1(proxy); ``` All bank calls in tests target `proxy`; retain `implementation = Upgrades.getImplementationAddress(proxy)` only for assertions. - [ ] Write failing admin tests for: - initialized asset, owner, unpaused state, zero liabilities, and version `1`; - initialization with zero asset; - initialization with zero owner through `OwnableInvalidOwner`; - proxy double initialization; - direct implementation initialization; - only owner can pause/unpause; - views still work while paused; - `_authorizeUpgrade` rejects a non-owner through `upgradeToAndCall`; - implementation `proxiableUUID()` is available directly while the proxy-context call reverts. - [ ] Run the focused suite and observe red: ```bash npm_config_offline=true forge test --match-path test/BankV1Admin.t.sol -vvv --force ``` Expected red: `BankV1` is missing. - [ ] Implement the minimum V1 skeleton with these imports and inheritance order: ```solidity import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; contract BankV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardTransient { IERC20 internal _asset; mapping(address account => uint256 balance) internal _balances; uint256 internal _totalLiabilities; uint256[47] private __gap; } ``` Use `error InvalidAsset(address asset);`. The initializer checks the asset before calling only `__Ownable_init(initialOwner)` and `__Pausable_init()`. `Initializable`, `UUPSUpgradeable`, and `ReentrancyGuardTransient` are stateless/shared in pinned OpenZeppelin 5.6.1 and have no initializer calls. The transient guard is required by the user-approved final design because it is constructor-free and the project's supported Anvil and Base Sepolia networks target Cancun/EIP-1153. - [ ] Add the only permitted validator annotation and no other bypass: ```solidity /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } ``` - [ ] Add `pause`/`unpause` with `onlyOwner`, simple views, `contractVersion() public pure virtual returns (uint256)`, and `_authorizeUpgrade(address) internal override onlyOwner {}`. Do not add deposits or withdrawals yet. - [ ] Run green verification and inspect storage: ```bash forge fmt npm_config_offline=true forge test --match-path test/BankV1Admin.t.sol -vvv --force forge inspect BankV1 storage-layout ``` Expected: tests pass and the application fields appear in the frozen order. OpenZeppelin namespaced/stateless internals must not be mistaken for permission to reorder application fields. - [ ] Commit: ```bash git add src/BankV1.sol test/helpers/BankTestBase.sol test/BankV1Admin.t.sol git commit -m "feat: establish UUPS bank V1" ``` --- ### 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: ```solidity 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 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`, 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: ```bash 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: ```solidity 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: ```solidity 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 Foundry’s replay seed on failure. - [ ] Run focused and aggregate green tests: ```bash 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: ```bash 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" ``` --- ### 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: ```solidity 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: ```bash 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: ```bash 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: ```bash git add test/helpers/BankHandler.sol test/BankInvariant.t.sol git commit -m "test: prove V1 accounting invariants" ``` --- ### Task 6: Deploy, seed, inspect, and export deterministic V1 state **Files:** - Create: `script/lib/DemoScript.sol` - Create: `script/DeployV1.s.sol` - Create: `script/SeedV1Demo.s.sol` - Create: `script/CheckState.s.sol` - Create: `test/ScriptPreflight.t.sol` - Create: `tools/finalize-manifest.mjs` - Create: `tools/select-manifest.mjs` - Create: `tools/test-finalize-manifest.mjs` - Create: `deployments/.gitkeep` - Modify: `Makefile` **Interfaces:** - Consumes an RPC, chain ID, explicit broadcaster, pending manifest, and Foundry broadcast receipts. - Produces an active schema-versioned public manifest only after receipt/code/postcondition checks, then the exact Act 1 state. - Establishes local actor indexes: owner `0`, Alice `1`, Bob `2` from Anvil’s standard development mnemonic. - [ ] Write `ScriptPreflight.t.sol` around a small public harness for `DemoScript` and cover: - chain IDs `31337` and `84532` accepted; - chain IDs `1`, `8453`, and an arbitrary value rejected before broadcast; - missing/invalid manifest, wrong manifest chain, zero address, and address-without-code rejected; - internal pending manifests may use deployment block `0`, while active manifests may not; - local keys derivable only when chain ID is `31337`; - serialized JSON contains public addresses but never the mnemonic, a private key, `PRIVATE_KEY`, or `MNEMONIC`. - [ ] Run red: ```bash forge test --match-path test/ScriptPreflight.t.sol -vvv --force ``` Expected red: `DemoScript` does not exist. - [ ] Implement `DemoScript` constants and guards: ```solidity uint256 internal constant ANVIL_CHAIN_ID = 31337; uint256 internal constant BASE_SEPOLIA_CHAIN_ID = 84532; string internal constant ANVIL_TEST_PHRASE = "test test test test test test test test test test test junk"; string internal constant PENDING_MANIFEST_PATH = "deployments/pending.json"; string internal constant ACTIVE_MANIFEST_PATH = "deployments/active.json"; error UnsupportedChain(uint256 chainId); error ManifestChainMismatch(uint256 expected, uint256 actual); error MissingCode(string label, address target); ``` Provide narrow helpers for chain checks, `vm.deriveKey` on local only, manifest read/write through `vm.parseJson*`/`vm.serialize*`, code checks, and exact-state assertions. Resolve the input/output manifest from `DEPLOYMENT_MANIFEST_PATH` with a narrow default appropriate to each script. Never log or serialize a derived private key. - [ ] Write `DeployV1.s.sol` red tests/behavior first, then implement: 1. validate chain and resolve the public `SCRIPT_SENDER` value; 2. start broadcast; 3. deploy `MockUSDC(sender)`; 4. call `Upgrades.deployUUPSProxy("BankV1.sol:BankV1", abi.encodeCall(...))`; 5. stop broadcast; 6. assert code, owner, asset, version `1`, and implementation identity; 7. write `deployments/pending.json` with schema `1`, network, chain, deployment block `0`, display RPC/explorer metadata, token, proxy, implementation, owner, and local actor labels/addresses. On local, derive account `0`, require it equals `SCRIPT_SENDER`, and broadcast with that derived development key. On Base Sepolia, require `SCRIPT_SENDER` equals the expected owner and use the signer selected by Forge’s matching `--account`/`--sender` options; never read a raw signing secret from environment. - [ ] Test `finalize-manifest.mjs` and `select-manifest.mjs` with temporary pending/broadcast fixtures and an injected fake JSON-RPC function. Cover: a pre-broadcast deploy guard that rejects an existing target-chain canonical file, exact proxy transaction match, successful receipt, receipt-derived block, actual chain-ID match, code at token/proxy/implementation, implementation-slot match, failed/missing/ambiguous receipt, partial broadcast, wrong chain, missing code, an existing active manifest from either network remaining untouched until selection, secret-bearing content, atomic same-directory rename, and selection of only a valid confirmed chain manifest. Prove a failure never creates/changes a confirmed or active manifest, and selecting Base preserves Anvil byte-for-byte (and vice versa). - [ ] Implement the finalizer using Node standard modules and JSON-RPC `fetch`. Its network-free `preflight-deploy ` fails before Forge runs when the target canonical file exists and prints the exact safe recovery command (`make reset-local` or `make archive-base-manifest`). Deploy finalization reads `broadcast/DeployV1.s.sol//run-latest.json`, matches the transaction whose created address is the pending proxy, matches its transaction hash to a successful receipt, uses that receipt’s real block number, queries `eth_chainId`, `eth_getCode` for all three contracts, and queries the EIP-1967 implementation slot. Atomically write the chain’s canonical `anvil.json` or `base-sepolia.json` only after all checks pass. `select-manifest.mjs` validates a named canonical file and atomically copies it to `active.json`; it never deletes or overwrites the other chain. Never copy a credential-bearing terminal RPC into JSON. - [ ] Write and implement `SeedV1Demo.s.sol`, hard-guarded to chain `31337`. It must perform and assert this exact sequence: ```text owner mints Alice 2,000 mUSDC owner mints Bob 1,000 mUSDC Alice approves and deposits 1,000 mUSDC Bob approves and deposits 500 mUSDC Alice withdraws 100 mUSDC ``` Postconditions: Alice internal `900e6`, Bob internal `500e6`, liabilities `1_400e6`, reserves `1_400e6`, version `1`, no surplus. - [ ] Write and implement `CheckState.s.sol`. Always print network, block, token, proxy, implementation, owner, pause state, version, each configured actor balance, reserves, liabilities, and surplus. Always fail on `reserves < liabilities`, implementation/manifest mismatch, or manifest chain mismatch. `DEMO_EXPECTED_STAGE=deployed` asserts version `1` and empty accounting; `v1` asserts exact Act 1 values; `invariants` checks network-independent invariants only. Task 11 adds stage `v2`. - [ ] Add direct Make targets that do not start background processes yet: ```make RPC_LOCAL := http://127.0.0.1:8545 ANVIL_OWNER := 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 .PHONY: deploy-v1 seed-v1 check-state deploy-v1: @node tools/finalize-manifest.mjs preflight-deploy anvil @SCRIPT_SENDER=$(ANVIL_OWNER) DEPLOYMENT_MANIFEST_PATH=deployments/pending.json npm_config_offline=true forge script script/DeployV1.s.sol:DeployV1 --rpc-url $(RPC_LOCAL) --sender $(ANVIL_OWNER) --broadcast --force @DEPLOYMENT_MANIFEST_PATH=deployments/pending.json DEMO_EXPECTED_STAGE=deployed forge script script/CheckState.s.sol:CheckState --rpc-url $(RPC_LOCAL) --force @node tools/finalize-manifest.mjs deploy --rpc-url $(RPC_LOCAL) @node tools/select-manifest.mjs anvil seed-v1: @forge script script/SeedV1Demo.s.sol:SeedV1Demo --rpc-url $(RPC_LOCAL) --broadcast --force check-state: @DEMO_EXPECTED_STAGE=$${DEMO_EXPECTED_STAGE:-v1} forge script script/CheckState.s.sol:CheckState --rpc-url $(RPC_LOCAL) --force ``` - [ ] Run unit/preflight green checks: ```bash forge fmt forge test --match-path test/ScriptPreflight.t.sol -vvv --force ``` - [ ] Run the first real local smoke test in two terminals. Terminal A: ```bash anvil --host 127.0.0.1 --port 8545 --chain-id 31337 ``` Terminal B: ```bash make deploy-v1 make seed-v1 DEMO_EXPECTED_STAGE=v1 make check-state ``` Expected: the exact Act 1 table prints and all commands exit `0`. Inspect identical `deployments/anvil.json` and selected `deployments/active.json`, verify their deployment block matches the confirmed proxy receipt, and verify they contain no secret material. Simulate a failed receipt fixture and confirm the finalizer changes neither confirmed nor active files. - [ ] Commit: ```bash git add script test/ScriptPreflight.t.sol tools/finalize-manifest.mjs tools/select-manifest.mjs tools/test-finalize-manifest.mjs deployments/.gitkeep Makefile git commit -m "feat: script deterministic V1 demo state" ``` --- ### Task 7: Build the artifact bridge and typed read model **Files:** - Create: `tools/sync-web-artifacts.mjs` - Create: `tools/test-sync-web-artifacts.mjs` - Create: `tools/publish-web-manifest.mjs` - Create: `web/src/generated/.gitkeep` - Create: `web/public/.gitkeep` - Create: `web/src/types/dashboard.ts` - Create: `web/src/config/chains.ts` - Create: `web/src/config/manifest.ts` - Create: `web/src/config/manifest.test.ts` - Create: `web/src/data/bankClient.ts` - Create: `web/src/data/bankClient.test.ts` - Modify: `Makefile` **Interfaces:** - ABI sync consumes only Foundry V1/token artifacts and works without RPC or a deployment manifest. - Manifest publishing separately consumes confirmed `deployments/active.json`. - Produces ignored `web/src/generated/contracts.ts` and `web/public/deployment.json` without mixing build and live-state requirements. - Produces `DashboardSnapshot` from a narrow injectable reader so web tests never need a live chain. - [ ] Define discriminated TypeScript types first: ```ts export type DeploymentManifest = Readonly<{ schemaVersion: 1 network: 'anvil' | 'baseSepolia' chainId: 31337 | 84532 deploymentBlock: bigint rpcUrl?: string explorerBaseUrl?: string token: Address proxy: Address implementation: Address owner: Address actors: readonly Readonly<{ label: string; address: Address }>[] }> export type DashboardSnapshot = Readonly<{ blockNumber: bigint synchronizedAt: Date version: 1 paused: boolean asset: Address owner: Address proxy: Address implementation: Address reserves: bigint liabilities: bigint surplus: bigint actors: readonly ActorBalance[] activity: readonly Activity[] diagnostics: readonly DecodeDiagnostic[] }> ``` Use tagged `Activity` variants only for V1-visible `deposit`, `withdrawal`, `paused`, `unpaused`, `ownershipTransferred`, and `upgraded`, each carrying block number, log index, transaction hash, and typed event fields. A malformed log becomes a separate `DecodeDiagnostic` containing block/log/transaction identity and a safe message; it is never an `Activity` and never removes valid events. Do not add a transfer variant or accept version `2` before `demo-start`; Task 11 implements that live. - [ ] Write `manifest.test.ts` before its parser. Test a valid local file, valid Base Sepolia file, malformed JSON, schema mismatch, chain/network mismatch, invalid address, zero contract address, empty/duplicate actor, deployment block below `1`, and credential-bearing RPC URLs containing user info or sensitive query keys (`key`, `token`, `secret`). The parser returns a typed value or an explanatory validation error; it never supplies defaults for missing addresses. - [ ] Run red: ```bash npm --prefix web test -- src/config/manifest.test.ts ``` Expected red: parser is missing. - [ ] Implement `parseDeploymentManifest(unknown)` with explicit field guards and viem `isAddress`. Enforce `anvil -> 31337` and `baseSepolia -> 84532`. Normalize addresses only with `getAddress`; preserve `undefined` for optional URLs. - [ ] Write `test-sync-web-artifacts.mjs` using Node’s built-in `assert` and temporary fixture directories. Prove that sync: - rejects a missing V1 or token artifact; - validates required V1 functions/events; - emits `bankV1Abi` and `mockUsdcAbi` using `as const`; - neither reads a manifest nor emits `bankV2Abi`; - makes `--check` fail when generated output is stale. Test `publish-web-manifest.mjs` separately in the same harness: only a schema-valid active manifest is copied, browser RPC is public/credential-free, pending/zero-block manifests are rejected, secret markers are rejected, and no address/default is fabricated. - [ ] Run red: ```bash node tools/test-sync-web-artifacts.mjs ``` Expected red: sync module is missing. - [ ] Implement `sync-web-artifacts.mjs` using only Node standard modules. Export pure `extractAbi`, `renderContractsModule`, and `syncArtifacts` functions for the test. Resolve every path from the repository root derived from `import.meta.url`, never the caller’s working directory. Write only `web/src/generated/contracts.ts`. Implement the separate publisher to validate/copy only `deployments/active.json` to `web/public/deployment.json`. - [ ] Write `bankClient.test.ts` against a `BankReader` test double. Cover: - all reads pinned to one latest block number; - `reader.getChainId()` compared with the manifest before any contract read; - nonempty bytecode at proxy, token, and implementation before state reads; - version, pause, owner, asset, liabilities, reserves, actor balances; - EIP-1967 implementation slot decoding; - implementation-slot/manifest mismatch; - asset/proxy/owner mismatch; - surplus as `reserves - liabilities`; - insolvency rejected instead of producing a negative surplus; - V1 event decoding from the proxy address only and deterministic `(blockNumber, logIndex)` ordering; - one malformed log becoming a `DecodeDiagnostic` while other logs render; - read rejection preserving the error rather than substituting `0n`. Use the canonical implementation slot: ```ts export const EIP1967_IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' ``` - [ ] Run red: ```bash npm --prefix web test -- src/data/bankClient.test.ts ``` Expected red: `BankReader`/snapshot loader is missing. - [ ] Implement a narrow `ViemBankReader` adapter and pure `loadDashboardSnapshot(reader, manifest)`. Call the endpoint’s `eth_chainId` and check code at proxy/token/implementation first. Read the latest block next and pass that block to every direct `readContract`, token-balance call, storage-slot read, and log range endpoint. The token reserve must come from `MockUSDC.balanceOf(proxy)`, not native ETH balance. Do not use `useReadContracts`/Multicall3 because a fresh Anvil chain does not guarantee a Multicall deployment. - [ ] Configure exactly two public clients in `chains.ts`: a custom Anvil chain with ID `31337`/`http://127.0.0.1:8545`, and viem’s Base Sepolia definition with ID `84532`. Select RPC from the validated manifest or `VITE_RPC_URL`; reject absence rather than inventing a remote provider. - [ ] Add bridge targets: ```make .PHONY: sync-abis publish-web-manifest sync-artifacts sync-artifacts-check sync-abis: @node tools/sync-web-artifacts.mjs publish-web-manifest: @node tools/publish-web-manifest.mjs sync-artifacts: sync-abis publish-web-manifest sync-artifacts-check: @node tools/test-sync-web-artifacts.mjs @node tools/sync-web-artifacts.mjs @node tools/sync-web-artifacts.mjs --check ``` - [ ] Run all green checks with a live V1 manifest from Task 6: ```bash forge build --force node tools/test-sync-web-artifacts.mjs make sync-abis make publish-web-manifest npm --prefix web test -- src/config/manifest.test.ts src/data/bankClient.test.ts npm --prefix web run typecheck ``` Expected: generated ABI/manifest exist locally, all tests pass, and `git status --short` does not list generated files. - [ ] Commit: ```bash git add tools/sync-web-artifacts.mjs tools/test-sync-web-artifacts.mjs tools/publish-web-manifest.mjs web/src/generated/.gitkeep web/public/.gitkeep web/src/types web/src/config web/src/data Makefile git commit -m "feat: bridge chain artifacts to typed web reads" ``` --- ### Task 8: Render the read-only V1 operations console **Files:** - Create: `web/src/main.tsx` - Create: `web/src/App.tsx` - Create: `web/src/App.test.tsx` - Create: `web/src/app.css` - Create: `web/src/hooks/useBankDashboard.ts` - Create: `web/src/hooks/useBankDashboard.test.tsx` - Create: `web/src/components/StatusHeader.tsx` - Create: `web/src/components/AccountingGrid.tsx` - Create: `web/src/components/ContractIdentity.tsx` - Create: `web/src/components/AccountTable.tsx` - Create: `web/src/components/ActivityTimeline.tsx` - Create: `web/src/components/WarningBanner.tsx` - Create: `web/src/components/TrustDisclosure.tsx` - Create: `web/src/components/format.ts` - Create: `web/src/components/format.test.ts` - Modify: `web/src/test/setup.ts` **Interfaces:** - Consumes the manifest parser, `DashboardSnapshot`, and block-triggered snapshot loader. - Produces loading, ready, stale, disconnected, invalid-manifest, and chain-mismatch screens. - Produces no transaction, signing, connector, form, or wallet interface. - [ ] Write `format.test.ts` first. Cover six-decimal amounts, shortened addresses, surplus, exact `100.00%` reserve ratio when equal, overcollateralized ratio, and zero-liability text `No deposits`. Failed/unknown values format as an em dash, never `0`. - [ ] Write `useBankDashboard.test.tsx` with an injected loader and controllable clock. Cover: - initial `loading`; - successful `ready` with last synchronized block/time; - later failure retaining the last snapshot as `stale`; - initial RPC failure as `disconnected` with no fabricated snapshot; - invalid manifest and manifest/network mismatch as terminal explanatory states; - a new watched block triggering exactly one reconciled refetch. - [ ] Write `App.test.tsx` using deterministic V1 snapshots. Assert: - persistent educational/mock/never-real-funds warning; - visible unaudited status, the owner’s power to install arbitrary future logic, UUPS state/bricking risk, and a statement that a real product needs professional audits, operational key controls, multisig or timelocked governance, incident procedures, legal advice, and jurisdiction-specific compliance; - network, block, ready/stale/disconnected, paused, and version status; - reserves, liabilities, surplus, reserve ratio, proxy, implementation, token, owner, and deployment block; - Alice/Bob labels locally and shortened addresses publicly; - deposit/withdraw/pause/ownership/upgrade events; - a decode diagnostic warning alongside still-visible valid activity; - Base Sepolia explorer links and no local explorer links; - `No deposits` for zero liabilities; - no `button`, `form`, wallet-connect text, deposit/withdraw action, or transaction control anywhere in the rendered document. - [ ] Run the three red suites: ```bash npm --prefix web test -- src/components/format.test.ts src/hooks/useBankDashboard.test.tsx src/App.test.tsx ``` Expected red: formatters, hook, and UI components are missing. - [ ] Implement the wagmi provider with exactly two chains, public HTTP transports, and `connectors: []`. Mount `QueryClientProvider` and `WagmiProvider`; do not import wallet packages or expose a `WalletClient`. The configured wagmi chain is UI/query configuration only; the data loader still verifies the RPC endpoint’s returned chain ID. - [ ] Implement `useBankDashboard` as the sole async coordinator. Use a watched block number to invalidate the snapshot query, fetch direct reads and proxy-only event logs at one block, retain the last successful snapshot on later failures, and mark it stale with the failing timestamp/error. Stop polling when the manifest is invalid or the RPC-reported chain differs from it. - [ ] Implement the six console regions plus an always-visible/semantic `TrustDisclosure`. Use semantic text/table/list markup, visible focus styles, responsive single-column fallbacks, and CSS custom properties. Tooltips must be keyboard-readable `` or always-visible helper text rather than action buttons. - [ ] Keep the visual hierarchy presentation-oriented: - dark neutral canvas and high-contrast green/amber/red status indicators; - proxy address visually primary, implementation visually secondary; - large reserves/liabilities figures with surplus/ratio below; - activity newest first while retaining block and transaction context; - no consumer-banking imagery or language suggesting regulated status. - [ ] Run green verification: ```bash npm --prefix web run lint npm --prefix web run typecheck npm --prefix web test npm --prefix web run build ``` Expected: all checks pass and Vite produces `web/dist`. - [ ] With Task 6’s Anvil running, run `make sync-artifacts` and `npm --prefix web run dev`; visually verify the exact Act 1 values and resize to desktop/tablet widths. Record any visual defect as a failing component test before correcting it. - [ ] Commit: ```bash git add web/src git commit -m "feat: add read-only bank operations console" ``` --- ### Task 9: Orchestrate, document, verify, and checkpoint the prepared V1 demo **Files:** - Modify: `tools/doctor.sh` - Create: `tools/process-lib.sh` - Create: `tools/demo-local.sh` - Create: `tools/reset-local.sh` - Create: `tools/test-process-safety.sh` - Create: `tools/scan-project.sh` - Modify: `Makefile` - Create: `README.md` - Create: `docs/LEARNING_GUIDE.md` - Create: `docs/PRESENTER_RUNBOOK.md` **Interfaces:** - Produces the supported user commands and the `demo-start` Git tag. - Consumes the V1 scripts, artifact sync, Vite app, and only project-owned `.demo/` PIDs. - Leaves the repository at the exact checkpoint from which the live Codex V2 change begins. - [ ] Write `test-process-safety.sh` first using temporary directories and harmless child processes. Assert: - an absent PID file is a no-op; - a stale numeric PID file is removed; - a nonnumeric PID is rejected; - a live PID whose command does not match the recorded `anvil`/`vite` signature is never signaled; - PID reuse is rejected by comparing the recorded `/proc//stat` start tick; - a matching project-started child is terminated and reaped; - an owned process group is stopped without leaving a Vite child, while an unrelated group survives; - reset deletes only known `.demo/{anvil,vite}.{pid,start,pgid,log}` files, Anvil staging/canonical files, an active/copied web manifest only when its parsed chain is `31337`, and the generated ABI module; - a Base canonical or active manifest survives `reset-local` byte-for-byte; - a sentinel adjacent to `.demo/` survives. - [ ] Run red: ```bash bash tools/test-process-safety.sh ``` Expected red: process library/reset script is missing. - [ ] Implement `process-lib.sh` with repository-root resolution, numeric PID/PGID validation, recorded process start tick, `/proc//cmdline` or `ps` signature checks, and TERM-then-bounded-KILL only for a matching recorded process group. Never use `pkill`, `killall`, a wildcard PID, or recursive workspace deletion. - [ ] Implement `reset-local.sh` by stopping validated PID files and removing each known runtime file by explicit path. Parse before deleting: remove `deployments/anvil.json`; remove staging/active/browser manifests only when they validate as chain `31337`; preserve every chain `84532` file. Use `rmdir .demo` only if empty. Report each removed process/file and that local generated state is reproducible. - [ ] Expand `doctor.sh` to check exact Foundry/Node/npm versions, recursive Git submodules, root/web `node_modules`, the pinned offline upgrades CLI, Bash, Make, `curl`, ports `8545`/`5173`, writable runtime directories, and optional Base variables. It must be read-only and print direct official installation links for missing prerequisites. - [ ] Implement `demo-local.sh`: 1. call the scoped reset helper, which can stop only previously recorded project-owned processes; 2. run doctor and refuse any still-occupied (therefore external) required port; 3. create `.demo/` with mode `0700`; 4. launch pinned Anvil in a new process group on `127.0.0.1:8545`, chain ID `31337`, standard development mnemonic, and record/validate its PID, PGID, and start tick; 5. wait with a bounded readiness loop using `cast chain-id`; 6. run deploy/finalization, seed, `DEMO_EXPECTED_STAGE=v1` state check, build, ABI sync, and confirmed-manifest publishing; 7. launch `web/node_modules/.bin/vite web --host 127.0.0.1 --port 5173` directly in its own process group (not through a parent npm shell), record/validate identity, wait for bounded HTTP readiness, then print the console URL and next commands; 8. install `INT`/`TERM`/`EXIT` traps and remain attached while both children are alive; 9. stop only its recorded children on exit. - [ ] Replace the Makefile with the complete V1 command contract: ```make .PHONY: doctor setup demo-local verify check-state reset-local doctor: @bash tools/doctor.sh setup: @git submodule update --init --recursive @npm ci @npm --prefix web ci demo-local: @bash tools/demo-local.sh verify: @forge fmt --check @forge clean @npm_config_offline=true forge build --force @node tools/sync-web-artifacts.mjs @node tools/sync-web-artifacts.mjs --check @node tools/check-upgrades-cli.mjs @npm_config_offline=true forge test --force @node tools/test-finalize-manifest.mjs @node tools/test-sync-web-artifacts.mjs @bash tools/test-process-safety.sh @bash tools/scan-project.sh @npm --prefix web run lint @npm --prefix web run typecheck @npm --prefix web test @npm --prefix web run build check-state: @forge script script/CheckState.s.sol:CheckState --rpc-url http://127.0.0.1:8545 --force reset-local: @bash tools/reset-local.sh ``` Retain the explicit `deploy-v1`, `seed-v1`, and `sync-artifacts` lower-level targets for teaching and recovery. - [ ] Write `README.md` with prerequisites, `make setup`, the ten-minute `make demo-local` quick start, second-terminal commands, architecture paragraph, exact URLs, command reference, and links to both guides. Its trust box must say the token has no value; code is educational/unaudited; real deposits must never be sent; the owner can pause and install arbitrary future logic; UUPS mistakes can corrupt state or brick upgrades; and a real custody product needs professional audits, operational key controls, multisig/timelocked governance, incident procedures, legal advice, and jurisdiction-specific compliance. - [ ] Write the V1-complete `LEARNING_GUIDE.md`: proxy/implementation/delegatecall/storage, constructor vs initializer, disabled implementation initialization, safe/unsafe storage examples, reserves/liabilities/surplus, exact deposit/withdraw flow, SafeERC20, pause, reentrancy, checks-effects-interactions, single-owner arbitrary-upgrade power, UUPS corruption/bricking risk, the complete real-product safeguards disclosure, and local exercises that trigger each named error. - [ ] Write the prepared portion of `PRESENTER_RUNBOOK.md`: preflight, rehearsal timing, exact three-act story, the approved live Codex prompt verbatim, Act 1 expected state, commands/console callouts, occupied-port/stale-console/test-failure recovery, and the rule that no upgrade runs until `make verify` passes. Describe Act 2/3 expected outcomes without including reference V2 source, and include a closing trust disclosure checklist matching README/UI word-for-word in substance. - [ ] Implement `scan-project.sh` with a NUL-safe array from `git ls-files`, excluding dependency gitlinks, `docs/superpowers`, and generated lockfiles. Return nonzero if project-owned tracked content contains an actual `PRIVATE_KEY`/`MNEMONIC` assignment, PEM private key, `TODO`, `TBD`, `FIXME`, filler text, or an unsafe upgrade bypass in `src`/`test`/`script`. Construct the scanner’s own pattern from split shell literals so it does not match itself. Permit only the exact hyphenated `oz-upgrades-unsafe-allow constructor` annotation approved for `BankV1`. The committed `ANVIL_TEST_PHRASE` is the universally known test fixture, not a supported secret input; test that exact fixture is the only scan exception. - [ ] Run process and full verification: ```bash bash tools/test-process-safety.sh make doctor make verify ``` Expected: all checks exit `0`. - [ ] Run a fresh presentation smoke test: ```bash make reset-local make demo-local ``` In a second terminal: ```bash DEMO_EXPECTED_STAGE=v1 make check-state curl --fail http://127.0.0.1:5173/ ``` Expected: exact Act 1 state, HTTP `200`, and the operations console shows V1. Stop the attached demo with Ctrl-C, then prove `make reset-local` leaves no owned process or runtime manifest. - [ ] Scan for forbidden material and placeholders: ```bash bash tools/scan-project.sh git diff --check git status --short ``` Expected: no secret assignment/value, unfinished placeholder, or whitespace error appears. Separately inspect chain constants/targets and confirm only `31337`/`84532` are accepted; tests may intentionally mention rejected mainnet IDs. - [ ] Commit and tag the live starting point: ```bash git add .gitignore Makefile tools README.md docs/LEARNING_GUIDE.md docs/PRESENTER_RUNBOOK.md git commit -m "docs: prepare repeatable V1 presentation" git tag -a demo-start -m "Verified V1 starting point for live UUPS demo" ``` Do not create the tag unless `make verify` and the fresh local smoke test have just passed. --- ### Task 10: Add BankV2 and prove upgrade safety before scripting it **Files:** - Create: `src/BankV2.sol` - Create: `test/BankV2.t.sol` - Create: `test/BankUpgrade.t.sol` - Create: `test/mocks/IncompatibleBank.sol` - Create: `test/mocks/NonUUPSImplementation.sol` **Interfaces:** - Consumes the exact V1 source/storage layout and validated V1 proxy fixture. - Produces `transferBalance(address,uint256)`, `BalanceTransferred`, `InvalidRecipient`, `SelfTransfer`, and `contractVersion() == 2`. - Produces no initializer and no storage variable. - [ ] Start `BankV2.t.sol` by upgrading a populated V1 proxy in test setup with the owner-aware overload: ```solidity Options memory opts; opts.referenceContract = "BankV1.sol:BankV1"; Upgrades.upgradeProxy(proxy, "BankV2.sol:BankV2", "", opts, owner); bankV2 = BankV2(proxy); ``` Use `--force` on every test command because validation consumes fresh build info. - [ ] Write failing transfer unit/fuzz tests for: - 250 mUSDC Alice-to-Bob balance deltas and exact event; - unchanged liabilities and token reserves; - zero amount; - zero recipient; - sender as recipient; - insufficient sender balance with available/requested values; - paused state; - recipients that previously had no balance; - fuzzed tracked recipients and amounts bounded to the sender balance; - deposit, withdrawal, views, pause, and unpause still behaving through the V2 proxy. - [ ] Run red: ```bash npm_config_offline=true forge test --match-path test/BankV2.t.sol -vvv --force ``` Expected red: `BankV2.sol` is missing. - [ ] Implement only this storage-free extension: ```solidity /// @custom:oz-upgrades-from src/BankV1.sol:BankV1 contract BankV2 is BankV1 { event BalanceTransferred(address indexed from, address indexed to, uint256 amount); error InvalidRecipient(address recipient); error SelfTransfer(); function transferBalance(address recipient, uint256 amount) external whenNotPaused { if (amount == 0) revert ZeroAmount(); if (recipient == address(0)) revert InvalidRecipient(recipient); if (recipient == msg.sender) revert SelfTransfer(); uint256 available = _balances[msg.sender]; if (amount > available) { revert InsufficientBalance(msg.sender, available, amount); } _balances[msg.sender] = available - amount; _balances[recipient] += amount; emit BalanceTransferred(msg.sender, recipient, amount); } function contractVersion() public pure override returns (uint256) { return 2; } } ``` Do not add `nonReentrant`: this function performs no external call. Do not consume the gap or redeclare inherited fields. - [ ] Write `BankUpgrade.t.sol` to populate Alice/Bob balances, create a reserve surplus, pause the bank, and snapshot proxy, implementation, owner, asset, pause state, both balances, liabilities, reserves, and surplus. Run a validated owner upgrade and assert: - proxy address unchanged; - implementation address changed and has code; - version changed exactly `1 -> 2`; - every other snapshot field is byte-for-byte equal, including paused state; - unpause plus every V1 mutation still works after upgrade; - implementation initialization still rejects. - [ ] Add three distinct failure cases: 1. non-owner `upgradeToAndCall` rejects with `OwnableUnauthorizedAccount`; 2. `Upgrades.validateUpgrade` rejects `IncompatibleBank`, which deliberately reorders/changes a V1 application storage field while remaining UUPS-capable; 3. a direct owner `upgradeToAndCall` to `NonUUPSImplementation` rejects at runtime through ERC-1822/UUPS compatibility. Do not weaken these tests with `UnsafeUpgrades`, `unsafeSkipStorageCheck`, `unsafeSkipAllChecks`, `unsafeAllow`, or annotation bypasses. - [ ] Run validated green suites and layout inspection: ```bash forge fmt npm_config_offline=true forge test --match-path test/BankV2.t.sol -vvv --force npm_config_offline=true forge test --match-path test/BankUpgrade.t.sol -vvv --force forge inspect BankV1 storage-layout forge inspect BankV2 storage-layout ``` Expected: all tests pass; V2 shows the same inherited application fields and no new application slot. - [ ] Run a source guard: ```bash if rg -n '(unsafeSkip|UnsafeUpgrades|unsafeAllow)' src test script; then echo "unsafe upgrade bypass found" >&2 exit 1 fi ``` Expected: no production/test use. The constructor’s narrowly scoped `oz-upgrades-unsafe-allow constructor` annotation is intentionally not matched by this expression. - [ ] Commit: ```bash git add src/BankV2.sol test/BankV2.t.sol test/BankUpgrade.t.sol test/mocks/IncompatibleBank.sol test/mocks/NonUUPSImplementation.sol git commit -m "feat: add storage-safe bank V2" ``` --- ### Task 11: Extend invariants, scripts, artifacts, and console for the live V2 change **Files:** - Create: `test/helpers/BankV2Handler.sol` - Modify: `test/BankInvariant.t.sol` - Create: `script/UpgradeV2.s.sol` - Create: `script/TransferV2Demo.s.sol` - Modify: `script/CheckState.s.sol` - Modify: `test/ScriptPreflight.t.sol` - Modify: `tools/finalize-manifest.mjs` - Modify: `tools/test-finalize-manifest.mjs` - Modify: `tools/sync-web-artifacts.mjs` - Modify: `tools/test-sync-web-artifacts.mjs` - Modify: `web/src/types/dashboard.ts` - Modify: `web/src/data/bankClient.ts` - Modify: `web/src/data/bankClient.test.ts` - Modify: `web/src/components/ActivityTimeline.tsx` - Modify: `web/src/App.test.tsx` - Modify: `Makefile` **Interfaces:** - Consumes V1 manifest/proxy and owner sender. - Produces a confirmed implementation update, exact Act 3 transfer, V2 activity decoding, and version-aware UI. - Updates only the manifest’s `implementation`; preserves schema, deployment block, proxy, token, owner, actors, and network. - [ ] Extend stateful testing first. `BankV2Handler` adds `transfer(uint256 fromSeed,uint256 toSeed,uint256 amount)`, restricts both endpoints to the tracked actor set, chooses a different recipient, returns early only when sender balance is zero, and bounds amount to `[1, senderBalance]`. Track `ghostTransferred` for action coverage but do not alter deposit/withdraw liability ghosts. - [ ] Add V2 invariants/assertions: - sum of every tracked actor balance equals liabilities; - reserves cover liabilities; - liabilities equal deposits minus withdrawals despite any number of transfers; - reserves equal deposits plus donations minus withdrawals; - at least one transfer executes in a deterministic handler unit test before relying on random invariant coverage. - [ ] Run red then green after the minimum handler implementation: ```bash npm_config_offline=true forge test --match-path test/BankInvariant.t.sol -vvv --force ``` - [ ] Write preflight tests for the upgrade script before the script: - manifest/proxy/code/chain/owner/version-1 requirements; - CLI sender must equal on-chain owner; - already-V2 path writes a verifiable `mode: "noop"` staging record, reports a no-op, and does not broadcast; - snapshot comparison rejects any changed field other than implementation/version; - manifest update rejects proxy/deployment-block/actor mutation. - [ ] Implement `UpgradeV2.s.sol` in this order: 1. validate chain before any broadcast; 2. load the active manifest, validate actual code and chain, and require public `SCRIPT_SENDER` equals proxy owner (the Make target supplies the identical value to Forge `--sender`); 3. if version is already `2`, write/overwrite a `mode: "noop"` staging record containing chain, observed block, owner nonce, proxy, and current implementation, then return without `vm.startBroadcast`; reject any other unexpected version; 4. snapshot proxy, implementation, owner, asset, paused, configured balances, liabilities, reserves, surplus, and deployment block; 5. set `Options.referenceContract = "BankV1.sol:BankV1"` and call `Upgrades.validateUpgrade("BankV2.sol:BankV2", opts)` before broadcast; 6. broadcast `Upgrades.upgradeProxy(proxy, "BankV2.sol:BankV2", "", opts)` from the verified owner; 7. stop broadcast and re-read every postcondition through the proxy; 8. for the normal path, write/overwrite `deployments/upgrade-pending.json` with `mode: "upgrade"`, the proposed implementation, confirmed-manifest identity, and the complete pre-upgrade snapshot; never write `active.json` directly. Extend the finalizer tests and implementation with upgrade/no-op modes. For `mode: "upgrade"`, match the successful transaction to the proxy and its `Upgraded(newImplementation)` log, require live `contractVersion() == 2`, require the live ERC-1967 slot to equal the pending implementation, and use artifact method identifiers plus direct JSON-RPC calls to compare owner, asset, pause state, every configured actor balance, liabilities, token reserves, surplus, proxy, and deployment block with the stored pre-upgrade snapshot. Atomically update only `implementation` in the selected chain’s canonical manifest and `active.json`. For `mode: "noop"`, deliberately ignore historical broadcast files, require the live block not precede the observation, require the owner nonce is unchanged, and require live version `2` plus chain/proxy/implementation equality with marker/active manifest; leave both confirmed files byte-for-byte unchanged. Include a stale prior `run-latest.json` fixture proving it is ignored. Delete the staging record only after either path succeeds. Any failed receipt/postcheck or unknown mode changes no confirmed file. - [ ] Implement `TransferV2Demo.s.sol`. On local it derives only Alice account index `1`, requires version `2`, asserts the pre-state Alice `900e6`/Bob `500e6`/liabilities-reserves `1_400e6`, snapshots liabilities/reserves, broadcasts `transferBalance(bob, 250e6)`, and asserts Alice `650e6`, Bob `750e6`, unchanged liabilities/reserves, and no surplus. - [ ] Extend `CheckState` with `DEMO_EXPECTED_STAGE=upgraded` for version `2` plus the pre-transfer Act 1 balances, and `v2` for the exact Act 3 table, while retaining `deployed`, `v1`, and `invariants` modes. The orchestration uses `invariants` after an idempotent upgrade because a valid already-V2 bank may have progressed past Act 3. - [ ] Extend the ABI bridge only now: require the real `BankV2` artifact, emit `bankV2Abi`, validate `transferBalance` and `BalanceTransferred`, and make freshness checks fail if the generated module remains V1-only. - [ ] Extend the dashboard types from `version: 1` to `version: 1 | 2` and add the `transfer` activity variant. Decode `BalanceTransferred` from proxy logs, render “Alice transferred 250.000000 mUSDC to Bob,” and add V2 snapshot/event tests. Keep all state reads through the proxy and keep the UI read-only. - [ ] Add exact local targets with owner/sender flags: ```make ANVIL_OWNER := 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 ANVIL_ALICE := 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 .PHONY: upgrade-v2 demo-transfer upgrade-v2: @SCRIPT_SENDER=$(ANVIL_OWNER) DEPLOYMENT_MANIFEST_PATH=deployments/upgrade-pending.json npm_config_offline=true forge script script/UpgradeV2.s.sol:UpgradeV2 --rpc-url $(RPC_LOCAL) --sender $(ANVIL_OWNER) --broadcast --force @node tools/finalize-manifest.mjs upgrade --rpc-url $(RPC_LOCAL) @DEMO_EXPECTED_STAGE=invariants forge script script/CheckState.s.sol:CheckState --rpc-url $(RPC_LOCAL) --force @node tools/sync-web-artifacts.mjs @node tools/publish-web-manifest.mjs demo-transfer: @forge script script/TransferV2Demo.s.sol:TransferV2Demo --rpc-url $(RPC_LOCAL) --sender $(ANVIL_ALICE) --broadcast --force @DEMO_EXPECTED_STAGE=v2 $(MAKE) check-state ``` After finalization, republish the web manifest because implementation identity changed. Keep shared finalization logic in one file; do not create an untested second upgrade finalizer. - [ ] Run the full V2 verification gate before broadcasting: ```bash make verify git diff --check ``` Expected: all Solidity validation/unit/fuzz/invariant/upgrade tests, artifact tests, process tests, and web checks pass. - [ ] Run the real Act 2/3 local smoke from the tagged V1 state: ```bash make upgrade-v2 make demo-transfer ``` Expected: proxy unchanged; implementation changed; version `2`; Alice `650e6`; Bob `750e6`; liabilities and reserves `1_400e6`; one decoded transfer; no token transfer during the internal move. - [ ] Commit: ```bash git add src test script tools web/src Makefile git commit -m "feat: demonstrate state-preserving V2 upgrade" ``` --- ### Task 12: Add the explicitly optional Base Sepolia encore **Files:** - Modify: `.env.example` - Modify: `script/lib/DemoScript.sol` - Modify: `script/DeployV1.s.sol` - Create: `script/SeedBaseSepolia.s.sol` - Modify: `script/UpgradeV2.s.sol` - Modify: `script/TransferV2Demo.s.sol` - Modify: `script/CheckState.s.sol` - Modify: `test/ScriptPreflight.t.sol` - Modify: `tools/finalize-manifest.mjs` - Modify: `tools/select-manifest.mjs` - Modify: `tools/publish-web-manifest.mjs` - Create: `tools/require-base-config.sh` - Create: `tools/test-base-config.sh` - Modify: `Makefile` - Modify: `README.md` - Modify: `docs/PRESENTER_RUNBOOK.md` **Interfaces:** - Consumes `BASE_SEPOLIA_RPC_URL`, `BASE_SEPOLIA_PUBLIC_RPC_URL`, `BASE_SEPOLIA_ACCOUNT`, `BASE_SEPOLIA_SENDER`, and `BASE_SEPOLIA_RECIPIENT`. - Produces explicit deploy/upgrade/transfer commands for chain `84532` using a named encrypted Foundry keystore. - Never makes a keystore, faucet, Base RPC call, or actual encore a prerequisite of `make verify` or local completion; only offline configuration tests join the gate. - [ ] Document public configuration only: ```dotenv # Terminal RPC may be credentialed; never copied into browser artifacts. BASE_SEPOLIA_RPC_URL= # Browser RPC is intentionally public and visible to browser users. BASE_SEPOLIA_PUBLIC_RPC_URL=https://sepolia.base.org BASE_SEPOLIA_ACCOUNT= BASE_SEPOLIA_SENDER= BASE_SEPOLIA_RECIPIENT= ``` State that keystore passwords are entered through Foundry’s interactive prompt and never stored in `.env`. - [ ] Add exact learner onboarding to the runbook before any Base command: ```bash cast wallet import uups-bank-base --interactive cast wallet address --account uups-bank-base ``` The presenter copies the displayed public address to `BASE_SEPOLIA_SENDER`, sets `BASE_SEPOLIA_ACCOUNT=uups-bank-base`, verifies both refer to the same account, funds only that Base Sepolia address with test ETH, and never pastes the key/password into Codex, shell history, or `.env`. - [ ] Extend preflight tests for chain `84532`, sender/owner equality, nonzero/different recipient, public browser RPC credential rejection, and all unsupported/mainnet chain IDs. Assert the guard runs before `vm.startBroadcast` in every testnet script path. - [ ] Before Base deployment/finalization, extend `DeployV1.s.sol` to require `BASE_SEPOLIA_SENDER`, `BASE_SEPOLIA_RECIPIENT`, and the intentionally public browser RPC, validate the actors as nonzero/different, and place both in the pending actor array as `Presenter` and `Recipient`. Serialize only `BASE_SEPOLIA_PUBLIC_RPC_URL`, never the terminal RPC. The active manifest is immutable deployment identity; seeding must never add actors after finalization. - [ ] Implement `SeedBaseSepolia.s.sol`: consume and verify the already-finalized presenter/recipient actors and sender, mint `1_000e6` valueless mUSDC to the presenter, approve/deposit `1_000e6`, and assert reserves/liabilities/presenter balance `1_000e6` plus recipient balance `0`. No Anvil test phrase or derived key path is reachable on Base Sepolia. - [ ] Extend `TransferV2Demo` testnet mode to transfer `250e6` from the verified presenter to the configured nonzero/different recipient, asserting unchanged reserves/liabilities and post-balances `750e6`/`250e6`. Keep the local exact path unchanged. - [ ] Extend manifest finalization to locate the matching chain-specific broadcast receipt, require receipt success, derive the real deployment/upgrade block, re-check code and EIP-1967 implementation, preserve the original deployment block on upgrade, and publish only through an atomic same-directory rename. - [ ] Extend `select-manifest.mjs` with a tested `archive baseSepolia` command that moves only `deployments/base-sepolia.json` to a validated timestamped sibling filename and never alters `active.json` or Anvil state. A subsequent deploy finalizer may create a new Base canonical file while the old Base active copy remains a fallback; only successful explicit selection replaces it. This gives a recoverable path before intentionally redeploying Base. Selecting either canonical manifest must update `active.json` and then the browser copy; refreshing the browser switches networks. - [ ] Add guarded Make helpers that fail when any required public variable is empty and then invoke Forge with both `--account` and `--sender`. Load an optional root `.env` through a narrowly documented Make include; it contains no signing secret or password: ```make -include .env export BASE_SEPOLIA_RPC_URL BASE_SEPOLIA_PUBLIC_RPC_URL BASE_SEPOLIA_ACCOUNT BASE_SEPOLIA_SENDER BASE_SEPOLIA_RECIPIENT .PHONY: deploy-base-sepolia upgrade-base-sepolia transfer-base-sepolia select-anvil select-base-sepolia archive-base-manifest select-anvil: @node tools/select-manifest.mjs anvil @node tools/publish-web-manifest.mjs select-base-sepolia: @node tools/select-manifest.mjs baseSepolia @node tools/publish-web-manifest.mjs archive-base-manifest: @node tools/select-manifest.mjs archive baseSepolia deploy-base-sepolia: @./tools/require-base-config.sh deploy @node tools/finalize-manifest.mjs preflight-deploy baseSepolia @SCRIPT_SENDER="$(BASE_SEPOLIA_SENDER)" DEPLOYMENT_MANIFEST_PATH=deployments/pending.json npm_config_offline=true forge script script/DeployV1.s.sol:DeployV1 --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --account "$(BASE_SEPOLIA_ACCOUNT)" --sender "$(BASE_SEPOLIA_SENDER)" --broadcast --slow --force @DEPLOYMENT_MANIFEST_PATH=deployments/pending.json DEMO_EXPECTED_STAGE=deployed forge script script/CheckState.s.sol:CheckState --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --force @node tools/finalize-manifest.mjs deploy --rpc-url "$(BASE_SEPOLIA_RPC_URL)" @SCRIPT_SENDER="$(BASE_SEPOLIA_SENDER)" DEPLOYMENT_MANIFEST_PATH=deployments/base-sepolia.json forge script script/SeedBaseSepolia.s.sol:SeedBaseSepolia --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --account "$(BASE_SEPOLIA_ACCOUNT)" --sender "$(BASE_SEPOLIA_SENDER)" --broadcast --slow --force @DEPLOYMENT_MANIFEST_PATH=deployments/base-sepolia.json DEMO_EXPECTED_STAGE=invariants forge script script/CheckState.s.sol:CheckState --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --force @node tools/select-manifest.mjs baseSepolia @node tools/sync-web-artifacts.mjs @node tools/publish-web-manifest.mjs upgrade-base-sepolia: @./tools/require-base-config.sh upgrade @SCRIPT_SENDER="$(BASE_SEPOLIA_SENDER)" DEPLOYMENT_MANIFEST_PATH=deployments/upgrade-pending.json npm_config_offline=true forge script script/UpgradeV2.s.sol:UpgradeV2 --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --account "$(BASE_SEPOLIA_ACCOUNT)" --sender "$(BASE_SEPOLIA_SENDER)" --broadcast --slow --force @node tools/finalize-manifest.mjs upgrade --rpc-url "$(BASE_SEPOLIA_RPC_URL)" @DEMO_EXPECTED_STAGE=invariants forge script script/CheckState.s.sol:CheckState --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --force @node tools/sync-web-artifacts.mjs @node tools/publish-web-manifest.mjs transfer-base-sepolia: @./tools/require-base-config.sh transfer @SCRIPT_SENDER="$(BASE_SEPOLIA_SENDER)" forge script script/TransferV2Demo.s.sol:TransferV2Demo --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --account "$(BASE_SEPOLIA_ACCOUNT)" --sender "$(BASE_SEPOLIA_SENDER)" --broadcast --slow --force @DEMO_EXPECTED_STAGE=invariants forge script script/CheckState.s.sol:CheckState --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --force ``` The repeated `SCRIPT_SENDER`/`--sender` values are deliberate: Solidity preflight validates the public expected signer, while Forge binds the actual encrypted-keystore signer. The finalizer receives the terminal RPC as a process argument but never prints or serializes it. - [ ] Add `tools/require-base-config.sh` and shell tests. Validate account name as a conservative identifier, sender/recipient as addresses, sender != recipient, both RPC schemes are HTTPS, browser URL has no user info or credential query, and terminal/public URL variables are never printed. Do not test a keystore password or accept `PRIVATE_KEY`/`MNEMONIC` fallbacks. - [ ] Append `bash tools/test-base-config.sh` to `make verify`. The test supplies only fake fixture values and never requires a keystore, RPC, faucet, or network. - [ ] Bind the console’s generic explorer-link support to the confirmed Base manifest and verify exact `https://sepolia.basescan.org/address/
`/transaction links. The browser uses only `BASE_SEPOLIA_PUBLIC_RPC_URL`; the terminal RPC never enters `web/public/deployment.json` or a Vite variable. - [ ] Run offline/dry verification without requiring faucet funds or a public RPC: ```bash make verify bash tools/test-base-config.sh make -n deploy-base-sepolia BASE_SEPOLIA_RPC_URL=https://terminal.invalid BASE_SEPOLIA_PUBLIC_RPC_URL=https://public.invalid BASE_SEPOLIA_ACCOUNT=demo BASE_SEPOLIA_SENDER=0x1111111111111111111111111111111111111111 BASE_SEPOLIA_RECIPIENT=0x2222222222222222222222222222222222222222 ``` Expected: tests pass; dry output contains `--account`, `--sender`, `--slow`, and no raw key option. It may print the deliberately fake URLs supplied to `make -n`; production helpers must not echo configured URLs. - [ ] If the user explicitly chooses to run the encore, use a funded Base Sepolia-only account and execute deploy, upgrade, transfer, and invariant-only state check. If faucet/RPC/explorer fails, record the optional failure and leave the completed local demo untouched. - [ ] Commit: ```bash git add .env.example script test/ScriptPreflight.t.sol tools Makefile README.md docs/PRESENTER_RUNBOOK.md git commit -m "feat: add guarded Base Sepolia encore" ``` --- ### Task 13: Finish learning materials and create the verified reference checkpoint **Files:** - Modify: `README.md` - Modify: `docs/LEARNING_GUIDE.md` - Modify: `docs/PRESENTER_RUNBOOK.md` **Interfaces:** - Produces the final audience/user handoff and annotated `demo-complete` tag. - Consumes actual verified commands/output from V1 and V2; documentation may not claim an unrun result. - [ ] Complete the learning guide with the V2 transfer conservation proof, upgrade snapshot, storage-layout validator explanation, why V2 has no initializer/storage, central owner threat model, and exercises for every V2 error. Include safe vs unsafe storage diagrams using the actual field order. - [ ] Complete the presenter runbook with: - exact preflight and ten-minute rehearsal; - two-terminal local sequence; - verbatim live Codex prompt; - what files should change in Act 2; - `make verify` evidence to point out; - Act 3 commands/values/event/identity callouts; - recovery for a failed edit/test, occupied port, stale UI, partial manifest, and unavailable Base services; - recovery from `demo-complete` in a new branch/worktree without resetting or overwriting unrelated work; - closing distinction between work performed by Codex and guarantees supplied by OpenZeppelin/Foundry. - [ ] Complete README commands and warnings. Verify every copied command against `make help`, ensure the quick start never requires Base Sepolia, a wallet browser extension, or a real secret, and cross-check the complete trust disclosures against the UI, learning guide, and presenter runbook. - [ ] Run the full static gate from a clean generated state: ```bash make reset-local make setup make verify git diff --check ``` Expected: all gates pass without a running chain or deployment manifest. - [ ] Run the full local three-act smoke on a fresh Anvil process: ```bash make demo-local ``` In the second terminal: ```bash DEMO_EXPECTED_STAGE=v1 make check-state make upgrade-v2 make demo-transfer DEMO_EXPECTED_STAGE=v2 make check-state curl --fail http://127.0.0.1:5173/ ``` Expected: exact Act 1 and Act 3 states; stable proxy; changed implementation; decoded transfer; HTTP `200`. - [ ] Stop the attached demo, then verify cleanup and repository safety: ```bash make reset-local bash tools/test-process-safety.sh bash tools/test-base-config.sh git diff --check git status --short ``` Expected: no Anvil/Vite child remains; only intentional source/document changes are present. - [ ] Scan only project-owned tracked files, excluding dependency gitlinks/locks and the approved design/plan prose. Review every match: ```bash bash tools/scan-project.sh ``` Expected: no secret assignment/value or placeholder; no unsafe upgrade bypass beyond the scanner’s single exact constructor-annotation allowance. - [ ] Inspect final evidence: ```bash git diff --stat demo-start..HEAD git log --oneline --decorate demo-start..HEAD git status --short ``` Confirm the V2 diff is bounded to the approved live prompt and that docs match actual output. - [ ] Commit final documentation and tag only after fresh verification: ```bash git add README.md docs/LEARNING_GUIDE.md docs/PRESENTER_RUNBOOK.md git commit -m "docs: finish UUPS bank demo guide" git tag -a demo-complete -m "Verified reference solution for UUPS bank demo" ``` Expected final history: `demo-start` points to the verified V1-only checkpoint; `demo-complete` points to the verified V2 reference solution. --- ## Final Acceptance Run The implementation is complete only when one fresh run records all of the following: ```bash make setup make verify make demo-local # terminal 1, remains attached DEMO_EXPECTED_STAGE=v1 make check-state make upgrade-v2 make demo-transfer DEMO_EXPECTED_STAGE=v2 make check-state make reset-local # after stopping terminal 1 ``` Evidence must show: - Act 1: Alice `900e6`, Bob `500e6`, liabilities/reserves `1_400e6`, version `1`. - Upgrade: identical proxy/owner/asset/pause/balances/liabilities/reserves and a changed implementation. - Act 3: Alice `650e6`, Bob `750e6`, liabilities/reserves `1_400e6`, version `2`, decoded internal transfer, and no reserve movement. - `make verify`: Solidity format/build/unit/fuzz/invariant/upgrade validation, artifact/shell safety checks, web lint/types/tests/build all green. - Cleanup: only project-owned processes/runtime files removed. - Git: `demo-start` contains no V2 source or transfer-aware UI; `demo-complete` contains the verified reference implementation.