196 lines
14 KiB
Markdown
196 lines
14 KiB
Markdown
# Presenter Runbook: Prepared V1 and Live UUPS Upgrade
|
||
|
||
> Educational demo — mock token — never use real funds.
|
||
|
||
## Exact preflight
|
||
|
||
Rehearse from a clean disposable branch or worktree created at the immutable `demo-start` tag. Use two terminals, keep the browser closed until Vite reports ready, and run this before the audience arrives:
|
||
|
||
```bash
|
||
make reset-local
|
||
make setup
|
||
make doctor
|
||
make verify
|
||
git diff --check
|
||
git status --short
|
||
```
|
||
|
||
The doctor must report Foundry `1.7.1`, Node `24.18.0`, npm `11.17.0`, initialized dependencies, writable runtime paths, and free ports `8545` and `5173`. `make verify` and `git diff --check` must exit `0`. At `demo-start`, tracked status must be clean. Do not upgrade after a partial or failed gate.
|
||
|
||
## Ten-minute rehearsal
|
||
|
||
Use this exact local timing once the dependencies are installed:
|
||
|
||
| Time | Action | Evidence to say aloud |
|
||
|---|---|---|
|
||
| `0:00–1:00` | Run the preflight checks and start `make demo-local` in terminal 1. | Anvil is chain `31337`; Vite and Anvil are project-owned and attached. |
|
||
| `1:00–3:00` | Run the V1 state check in terminal 2 and open the console. | Stable proxy, distinct implementation, version `1`, Alice `900`, Bob `500`, reserves = liabilities = `1,400 mUSDC`. |
|
||
| `3:00–5:30` | Deliver the live Codex prompt and review the prepared Act 2 diff. | V2 inherits V1, adds no storage or initializer, and the browser remains read-only. |
|
||
| `5:30–7:00` | Point to the completed `make verify` evidence, then run `make upgrade-v2`. | All gates passed before broadcast; proxy and state remain stable while implementation and version change. |
|
||
| `7:00–9:00` | Run `make demo-transfer`, the V2 check, and the HTTP check. | Alice `650`, Bob `750`, reserves and liabilities still `1,400`; one decoded internal transfer and no token transfer. |
|
||
| `9:00–10:00` | Read the trust disclosure, Ctrl-C terminal 1, and run `make reset-local`. | The owner remains central; local cleanup targets only validated project processes and artifacts. |
|
||
|
||
If a live Codex edit takes longer, treat this as the prepared reference rehearsal and allow a separate coding block. The verification-before-upgrade rule never changes to meet the clock.
|
||
|
||
## Two-terminal three-act sequence
|
||
|
||
### Act 1 — Prepared V1 establishes trust
|
||
|
||
Terminal 1 remains attached:
|
||
|
||
```bash
|
||
make demo-local
|
||
```
|
||
|
||
Terminal 2:
|
||
|
||
```bash
|
||
DEMO_EXPECTED_STAGE=v1 make check-state
|
||
curl --fail http://127.0.0.1:5173/
|
||
```
|
||
|
||
Open `http://127.0.0.1:5173/`; Anvil is at `http://127.0.0.1:8545`. Call out that every application read and write targets the proxy. The implementation address is separate and displayed only to explain delegation. The expected state is Alice `900 mUSDC`, Bob `500 mUSDC`, liabilities `1,400 mUSDC`, reserves `1,400 mUSDC`, surplus `0`, pause state `false`, and version `1`.
|
||
|
||
The scripts minted `2,000 mUSDC` to Alice and `1,000 mUSDC` to Bob; Alice deposited `1,000` and withdrew `100`; Bob deposited `500`. The stable proxy holds the reserves and application storage. Point to the deposits, withdrawal, initial ownership, and upgrade-to-V1 events, then state that the console is read-only and never signs.
|
||
|
||
### Act 2 — Codex changes the system live
|
||
|
||
Give Codex this approved prompt verbatim:
|
||
|
||
> Add `BankV2` with customer-to-customer internal transfers. Preserve the UUPS storage layout and all V1 behavior. Add unit, fuzz, invariant, and upgrade-regression tests; an owner upgrade script; a scripted Alice-to-Bob transfer; exported ABI support; and the read-only console updates needed to show V2 and its transfer event. Explain each security decision. Do not perform an upgrade until all verification passes.
|
||
|
||
The bounded local Act 2 diff from `demo-start` consists of these files:
|
||
|
||
- Contracts and direct tests: `src/BankV2.sol`, the initializer-visibility compatibility edit in `src/BankV1.sol`, `test/BankV2.t.sol`, `test/BankUpgrade.t.sol`, `test/mocks/IncompatibleBank.sol`, and `test/mocks/NonUUPSImplementation.sol`.
|
||
- Upgrade/demo/invariant path: `Makefile`, `script/CheckState.s.sol`, `script/UpgradeV2.s.sol`, `script/TransferV2Demo.s.sol`, `test/BankInvariant.t.sol`, `test/ScriptPreflight.t.sol`, and `test/helpers/BankV2Handler.sol`.
|
||
- Manifest and ABI bridge: `tools/finalize-manifest.mjs`, `tools/sync-web-artifacts.mjs`, `tools/test-finalize-manifest.mjs`, and `tools/test-sync-web-artifacts.mjs`.
|
||
- Read-only console: `web/src/App.test.tsx`, `web/src/components/ActivityTimeline.tsx`, `web/src/data/bankClient.ts`, `web/src/data/bankClient.test.ts`, and `web/src/types/dashboard.ts`.
|
||
|
||
The V1 compatibility edit changes `initialize` visibility from `external` to `public` so the pinned upgrades-core tooling recognizes the initializer as inherited by `BankV2`; it does not change the external ABI, initializer guard, or stored state. Generated ABI and deployment files may appear during execution but are reproducible runtime artifacts, not live source edits. The optional Base Sepolia files are a later encore and are not required for the local Act 2 story.
|
||
|
||
Ask Codex to show the diff and explain layout preservation, owner authorization, transfer conservation, error paths, snapshot postconditions, and why the browser remains read-only. Then run:
|
||
|
||
```bash
|
||
make verify
|
||
```
|
||
|
||
Point to each successful layer in the output: `forge fmt --check`; clean forced build; generated ABI freshness; the pinned OpenZeppelin upgrades CLI; 102 Solidity unit, fuzz, invariant, upgrade-regression, and script tests; 16 manifest-finalizer tests; 26 process-safety cases; offline Base-config and project-scanner gates; then web lint, TypeScript checking, 55 Vitest tests across six files, and the production Vite build. In the upgrade tests, highlight the validator accepting `BankV1` → `BankV2`, rejecting an incompatible layout, rejecting non-UUPS logic, and preserving the snapshot. If any layer fails, do not upgrade.
|
||
|
||
### Act 3 — V2 proves continuity
|
||
|
||
Continue in terminal 2 only after the complete gate passes:
|
||
|
||
```bash
|
||
make upgrade-v2
|
||
make demo-transfer
|
||
DEMO_EXPECTED_STAGE=v2 make check-state
|
||
curl --fail http://127.0.0.1:5173/
|
||
```
|
||
|
||
At the upgrade boundary, compare the printed identities and state:
|
||
|
||
- the proxy address is exactly the Act 1 proxy;
|
||
- the implementation address is different;
|
||
- owner, asset, pause state, Alice/Bob addresses and balances, liabilities, reserves, surplus, and deployment block are unchanged;
|
||
- version changes from `1` to `2`; and
|
||
- the upgrade finalizer matches the `Upgraded` event and live ERC-1967 implementation slot before publishing the V2 manifest.
|
||
|
||
After the transfer, call out Alice `650 mUSDC`, Bob `750 mUSDC`, liabilities `1,400 mUSDC`, reserves `1,400 mUSDC`, surplus `0`, and version `2`. The timeline must decode `BalanceTransferred(Alice, Bob, 250e6)` as “Alice transferred 250.000000 mUSDC to Bob.” That transaction has no MockUSDC `Transfer` log, and the unchanged reserves prove no ERC-20 left or entered the proxy. Refresh only if the console has not observed the new block; normally its live synchronization should show the change.
|
||
|
||
## Recovery without broad cleanup
|
||
|
||
- **Failed edit or test:** do not upgrade. Keep the failing command and diff visible, let Codex diagnose on the disposable live branch, or finish with the verified V1. If time expires, create a new disposable branch/worktree from `demo-start`; never reset or overwrite unrelated work.
|
||
- **Occupied port:** `make demo-local` refuses to claim either port. Probe with `curl http://127.0.0.1:8545` and `curl http://127.0.0.1:5173/`, then use operating-system process inspection to identify the external owner. Stop it only after proving ownership. The project never uses broad process matching.
|
||
- **Recorded stale process:** run `make reset-local`. It validates numeric PID, process group, command signature, and Linux start tick before signaling. A mismatched live process is preserved and reset exits nonzero.
|
||
- **Stale UI:** run the stage-appropriate check (`DEMO_EXPECTED_STAGE=v1 make check-state` before transfer or `DEMO_EXPECTED_STAGE=v2 make check-state` after it), inspect `.demo/vite.log`, and use `curl --fail http://127.0.0.1:5173/`. A stale/disconnected console preserves the last good snapshot and labels it; it never invents zeros.
|
||
- **Partial local manifest:** never copy `pending.json` or `upgrade-pending.json` into a confirmed manifest and never hand-edit `active.json`. If an upgrade broadcast succeeded but a transient finalizer step failed, preserve the staging and broadcast files, diagnose the cause, and rerun `node tools/finalize-manifest.mjs upgrade --rpc-url http://127.0.0.1:8545`; then run `make sync-artifacts`. Otherwise stop the attached process, run `make reset-local`, and restart from a fresh V1. Confirmed manifests remain unchanged on a failed finalization.
|
||
- **Unexpected child exit:** the attached launcher stops its other validated group. Inspect `.demo/anvil.log` and `.demo/vite.log`, run `make reset-local`, and restart.
|
||
- **Unavailable Base service:** stop the optional encore, record whether the faucet, RPC, or explorer failed, and leave the completed local result untouched. Never substitute another chain or expose a signing secret to rescue an encore.
|
||
|
||
End the local session with Ctrl-C in terminal 1, then run:
|
||
|
||
```bash
|
||
make reset-local
|
||
bash tools/test-process-safety.sh
|
||
bash tools/test-base-config.sh
|
||
git diff --check
|
||
git status --short
|
||
```
|
||
|
||
### Recover the verified reference without touching current work
|
||
|
||
`demo-complete` is a reference/recovery checkpoint, not a reason to reset the current checkout. From the repository root, first verify the tag and inspect current work:
|
||
|
||
```bash
|
||
git cat-file -t demo-complete
|
||
git tag -n99 demo-complete
|
||
git rev-parse 'demo-complete^{}'
|
||
git status --short
|
||
```
|
||
|
||
The type must be `tag`, the annotation must read `Verified reference solution for UUPS bank demo`, and the peeled target is the verified documentation commit. This checks the required annotated tag without implying that it carries a GPG signature.
|
||
|
||
Then create a new branch in a new worktree; choose unused names if either example name already exists:
|
||
|
||
```bash
|
||
git worktree add -b recovery/uups-bank-demo .worktrees/uups-bank-demo-recovery demo-complete
|
||
git -C .worktrees/uups-bank-demo-recovery status --short
|
||
```
|
||
|
||
This peels the annotated tag into a new branch without switching, resetting, cleaning, stashing, or overwriting the current worktree. Never delete an existing recovery directory or move either demo tag as part of recovery.
|
||
|
||
## Optional Base Sepolia encore
|
||
|
||
Run this only after the completed local demo and only when the presenter explicitly chooses the public encore. Create a named encrypted Foundry keystore before any Base deployment, upgrade, transfer, selection, or archive command:
|
||
|
||
```bash
|
||
cast wallet import uups-bank-base --interactive
|
||
cast wallet address --account uups-bank-base
|
||
```
|
||
|
||
Copy the displayed public address to `BASE_SEPOLIA_SENDER`, set `BASE_SEPOLIA_ACCOUNT=uups-bank-base`, and choose a different nonzero public address for `BASE_SEPOLIA_RECIPIENT`. The Base manifest schema is exactly two nested actors in order: `Presenter` at the configured sender, then `Recipient` at the configured recipient. Fund only the displayed Base Sepolia sender with Base Sepolia test ETH. Never paste the key or password into Codex, shell history, or `.env`; Foundry requests the encrypted-keystore password interactively.
|
||
|
||
Copy `.env.example` to `.env` and configure only public identifiers and endpoints:
|
||
|
||
```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=uups-bank-base
|
||
BASE_SEPOLIA_SENDER=
|
||
BASE_SEPOLIA_RECIPIENT=
|
||
```
|
||
|
||
The Base commands require HTTPS RPCs, reject browser URL credentials, validate the named account identifier and distinct nonzero actors, and bind both `--account` and `--sender`. They never accept a raw key or mnemonic. First run the offline gate and inspect the fake-value dry run:
|
||
|
||
```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
|
||
```
|
||
|
||
The dry run must contain `--account`, `--sender`, and `--slow`, and no raw-key option. The URLs are deliberately fake; these checks need no keystore, faucet, or network. Only with explicit authorization and Base Sepolia test funds may the presenter run:
|
||
|
||
```bash
|
||
make deploy-base-sepolia
|
||
make upgrade-base-sepolia
|
||
make transfer-base-sepolia
|
||
```
|
||
|
||
Deployment mints valueless mUSDC only to Presenter and deposits `1,000 mUSDC`; Recipient starts at `0`. The V2 internal transfer produces Presenter `750 mUSDC` and Recipient `250 mUSDC`, while reserves and liabilities remain `1,000 mUSDC`. Each command runs an invariant-only state check. The confirmed browser manifest contains only the intentionally public browser RPC and `https://sepolia.basescan.org`; the terminal RPC is never serialized.
|
||
|
||
Before an intentional Base redeployment, run `make archive-base-manifest`. It moves only `deployments/base-sepolia.json` to a validated timestamped sibling and leaves `active.json` and Anvil state untouched. A subsequent finalizer may create a new Base canonical manifest; the old active copy remains the browser fallback until `make select-base-sepolia` succeeds. Use `make select-anvil` to return the browser to the local manifest.
|
||
|
||
## Closing trust disclosure and tool boundaries
|
||
|
||
Read these points while the matching console panel is visible:
|
||
|
||
- MockUSDC has no value.
|
||
- These contracts are educational and unaudited; real deposits must never be sent here.
|
||
- The owner can pause customer actions and install arbitrary future logic.
|
||
- UUPS mistakes can corrupt state or permanently brick upgradeability.
|
||
- A real custody product requires professional audits, operational key controls, multisig or timelocked governance, incident procedures, legal advice, and jurisdiction-specific compliance work.
|
||
|
||
Codex performs the repository-specific work: application and UI implementation, tests, scripts, lifecycle orchestration, manifest handling, and explanation. OpenZeppelin supplies reviewed reusable primitives and a validator that checks declared upgrade/storage compatibility; it does not audit this application's economics, owner choices, or operations. Foundry compiles and executes the declared tests and scripts and provides local Anvil; it guarantees neither test completeness nor production safety. None of Codex, OpenZeppelin, Foundry, or a passing gate turns this teaching artifact into audited, decentralized, or regulated custody software.
|