14 KiB
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:
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:
make demo-local
Terminal 2:
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
BankV2with 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 insrc/BankV1.sol,test/BankV2.t.sol,test/BankUpgrade.t.sol,test/mocks/IncompatibleBank.sol, andtest/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, andtest/helpers/BankV2Handler.sol. - Manifest and ABI bridge:
tools/finalize-manifest.mjs,tools/sync-web-artifacts.mjs,tools/test-finalize-manifest.mjs, andtools/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, andweb/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:
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:
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
1to2; and - the upgrade finalizer matches the
Upgradedevent 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-localrefuses to claim either port. Probe withcurl http://127.0.0.1:8545andcurl 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-statebefore transfer orDEMO_EXPECTED_STAGE=v2 make check-stateafter it), inspect.demo/vite.log, and usecurl --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.jsonorupgrade-pending.jsoninto a confirmed manifest and never hand-editactive.json. If an upgrade broadcast succeeded but a transient finalizer step failed, preserve the staging and broadcast files, diagnose the cause, and rerunnode tools/finalize-manifest.mjs upgrade --rpc-url http://127.0.0.1:8545; then runmake sync-artifacts. Otherwise stop the attached process, runmake 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.logand.demo/vite.log, runmake 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:
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:
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:
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:
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:
# 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:
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:
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.