docs: track superpowers working documents in git
The uups-bank-demo wave's SDD records (ledger, six task briefs and reports, review diffs) and the brainstorm design mockups were git-ignored, so they existed only on one sandbox VM and reached no remote — this repo had no remote at all until now. Removes `.superpowers/` from .gitignore and the `*` .gitignore the superpowers plugin writes inside .superpowers/sdd/; the second blocks the directory even with the first removed. Excluded as ephemeral local-server state, and now ignored by name: .last-port, .last-token (a 64-char session token for a brainstorm server on a port that is long gone), and the per-session state/ directories. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fnwzj6McD6kSkXwjUKFKxe
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
# Review package: 1f4175ba729fbbab58538cd3ac02b5459e014c86..0664eb4688bd236e2678c5dda2f4a10e898205ed
|
||||
|
||||
## Commits
|
||||
0664eb4 feat: establish UUPS bank V1
|
||||
|
||||
## Files changed
|
||||
.../superpowers/plans/2026-08-17-uups-bank-demo.md | 10 +--
|
||||
src/BankV1.sol | 60 ++++++++++++++
|
||||
test/BankV1Admin.t.sol | 94 ++++++++++++++++++++++
|
||||
test/helpers/BankTestBase.sol | 33 ++++++++
|
||||
4 files changed, 192 insertions(+), 5 deletions(-)
|
||||
|
||||
## Diff
|
||||
diff --git a/docs/superpowers/plans/2026-08-17-uups-bank-demo.md b/docs/superpowers/plans/2026-08-17-uups-bank-demo.md
|
||||
index 3a3ca8f..cfaaaa6 100644
|
||||
--- a/docs/superpowers/plans/2026-08-17-uups-bank-demo.md
|
||||
+++ b/docs/superpowers/plans/2026-08-17-uups-bank-demo.md
|
||||
@@ -7,21 +7,21 @@
|
||||
**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 the constructor annotation immediately above `_disableInitializers()`.
|
||||
+- `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
|
||||
|
||||
@@ -367,39 +367,39 @@ All bank calls in tests target `proxy`; retain `implementation = Upgrades.getImp
|
||||
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 {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.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,
|
||||
- ReentrancyGuard
|
||||
+ 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 `ReentrancyGuard` are stateless/shared in pinned OpenZeppelin 5.6.1 and have no initializer calls.
|
||||
+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();
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1074,21 +1074,21 @@ reset-local:
|
||||
```
|
||||
|
||||
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 constructor annotation. 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.
|
||||
+- [ ] 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`.
|
||||
diff --git a/src/BankV1.sol b/src/BankV1.sol
|
||||
new file mode 100644
|
||||
index 0000000..13ffca5
|
||||
--- /dev/null
|
||||
+++ b/src/BankV1.sol
|
||||
@@ -0,0 +1,60 @@
|
||||
+// SPDX-License-Identifier: MIT
|
||||
+pragma solidity 0.8.35;
|
||||
+
|
||||
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
+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 {
|
||||
+ error InvalidAsset(address asset);
|
||||
+
|
||||
+ IERC20 internal _asset;
|
||||
+ mapping(address account => uint256 balance) internal _balances;
|
||||
+ uint256 internal _totalLiabilities;
|
||||
+ uint256[47] private __gap;
|
||||
+
|
||||
+ /// @custom:oz-upgrades-unsafe-allow constructor
|
||||
+ constructor() {
|
||||
+ _disableInitializers();
|
||||
+ }
|
||||
+
|
||||
+ function initialize(address asset_, address initialOwner) external initializer {
|
||||
+ if (asset_ == address(0)) {
|
||||
+ revert InvalidAsset(asset_);
|
||||
+ }
|
||||
+
|
||||
+ __Ownable_init(initialOwner);
|
||||
+ __Pausable_init();
|
||||
+
|
||||
+ _asset = IERC20(asset_);
|
||||
+ }
|
||||
+
|
||||
+ function pause() external onlyOwner {
|
||||
+ _pause();
|
||||
+ }
|
||||
+
|
||||
+ function unpause() external onlyOwner {
|
||||
+ _unpause();
|
||||
+ }
|
||||
+
|
||||
+ function asset() external view returns (IERC20) {
|
||||
+ return _asset;
|
||||
+ }
|
||||
+
|
||||
+ function balanceOf(address account) external view returns (uint256) {
|
||||
+ return _balances[account];
|
||||
+ }
|
||||
+
|
||||
+ function totalLiabilities() external view returns (uint256) {
|
||||
+ return _totalLiabilities;
|
||||
+ }
|
||||
+
|
||||
+ function contractVersion() public pure virtual returns (uint256) {
|
||||
+ return 1;
|
||||
+ }
|
||||
+
|
||||
+ function _authorizeUpgrade(address) internal override onlyOwner {}
|
||||
+}
|
||||
diff --git a/test/BankV1Admin.t.sol b/test/BankV1Admin.t.sol
|
||||
new file mode 100644
|
||||
index 0000000..7e3c26c
|
||||
--- /dev/null
|
||||
+++ b/test/BankV1Admin.t.sol
|
||||
@@ -0,0 +1,94 @@
|
||||
+// SPDX-License-Identifier: MIT
|
||||
+pragma solidity 0.8.35;
|
||||
+
|
||||
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
+import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
|
||||
+import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
|
||||
+import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
||||
+
|
||||
+import {BankV1} from "../src/BankV1.sol";
|
||||
+import {BankTestBase} from "./helpers/BankTestBase.sol";
|
||||
+
|
||||
+contract BankV1AdminTest is BankTestBase {
|
||||
+ bytes32 private constant ERC1967_IMPLEMENTATION_SLOT =
|
||||
+ 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
|
||||
+
|
||||
+ function testProxyStartsWithConfiguredAdministrationAndEmptyAccounting() public view {
|
||||
+ assertEq(address(bank.asset()), address(token));
|
||||
+ assertEq(bank.owner(), owner);
|
||||
+ assertFalse(bank.paused());
|
||||
+ assertEq(bank.balanceOf(alice), 0);
|
||||
+ assertEq(bank.totalLiabilities(), 0);
|
||||
+ assertEq(bank.contractVersion(), 1);
|
||||
+ }
|
||||
+
|
||||
+ function testInitializationChecksZeroAssetBeforeZeroOwner() public {
|
||||
+ vm.expectRevert(abi.encodeWithSelector(BankV1.InvalidAsset.selector, address(0)));
|
||||
+ new ERC1967Proxy(implementation, abi.encodeCall(BankV1.initialize, (address(0), address(0))));
|
||||
+ }
|
||||
+
|
||||
+ function testInitializationRejectsZeroOwner() public {
|
||||
+ vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableInvalidOwner.selector, address(0)));
|
||||
+ new ERC1967Proxy(implementation, abi.encodeCall(BankV1.initialize, (address(token), address(0))));
|
||||
+ }
|
||||
+
|
||||
+ function testProxyCannotBeInitializedTwice() public {
|
||||
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
|
||||
+ bank.initialize(address(token), owner);
|
||||
+ }
|
||||
+
|
||||
+ function testImplementationCannotBeInitialized() public {
|
||||
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
|
||||
+ BankV1(implementation).initialize(address(token), owner);
|
||||
+ }
|
||||
+
|
||||
+ function testOwnerCanPauseAndUnpause() public {
|
||||
+ vm.prank(owner);
|
||||
+ bank.pause();
|
||||
+ assertTrue(bank.paused());
|
||||
+
|
||||
+ vm.prank(owner);
|
||||
+ bank.unpause();
|
||||
+ assertFalse(bank.paused());
|
||||
+ }
|
||||
+
|
||||
+ function testNonOwnerCannotPause() public {
|
||||
+ vm.prank(stranger);
|
||||
+ vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, stranger));
|
||||
+ bank.pause();
|
||||
+ }
|
||||
+
|
||||
+ function testNonOwnerCannotUnpause() public {
|
||||
+ vm.prank(owner);
|
||||
+ bank.pause();
|
||||
+
|
||||
+ vm.prank(stranger);
|
||||
+ vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, stranger));
|
||||
+ bank.unpause();
|
||||
+ }
|
||||
+
|
||||
+ function testViewsRemainAvailableWhilePaused() public {
|
||||
+ vm.prank(owner);
|
||||
+ bank.pause();
|
||||
+
|
||||
+ assertEq(address(bank.asset()), address(token));
|
||||
+ assertEq(bank.balanceOf(alice), 0);
|
||||
+ assertEq(bank.totalLiabilities(), 0);
|
||||
+ assertEq(bank.contractVersion(), 1);
|
||||
+ }
|
||||
+
|
||||
+ function testNonOwnerCannotAuthorizeUpgrade() public {
|
||||
+ vm.prank(stranger);
|
||||
+ vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, stranger));
|
||||
+ bank.upgradeToAndCall(implementation, "");
|
||||
+ }
|
||||
+
|
||||
+ function testImplementationExposesERC1967ProxiableUUID() public view {
|
||||
+ assertEq(BankV1(implementation).proxiableUUID(), ERC1967_IMPLEMENTATION_SLOT);
|
||||
+ }
|
||||
+
|
||||
+ function testProxyRejectsProxiableUUIDCall() public {
|
||||
+ vm.expectRevert(UUPSUpgradeable.UUPSUnauthorizedCallContext.selector);
|
||||
+ bank.proxiableUUID();
|
||||
+ }
|
||||
+}
|
||||
diff --git a/test/helpers/BankTestBase.sol b/test/helpers/BankTestBase.sol
|
||||
new file mode 100644
|
||||
index 0000000..9287cc4
|
||||
--- /dev/null
|
||||
+++ b/test/helpers/BankTestBase.sol
|
||||
@@ -0,0 +1,33 @@
|
||||
+// SPDX-License-Identifier: MIT
|
||||
+pragma solidity 0.8.35;
|
||||
+
|
||||
+import {Test} from "forge-std/Test.sol";
|
||||
+import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
|
||||
+
|
||||
+import {BankV1} from "../../src/BankV1.sol";
|
||||
+import {MockUSDC} from "../../src/MockUSDC.sol";
|
||||
+
|
||||
+abstract contract BankTestBase is Test {
|
||||
+ address internal owner;
|
||||
+ address internal alice;
|
||||
+ address internal bob;
|
||||
+ address internal stranger;
|
||||
+
|
||||
+ MockUSDC internal token;
|
||||
+ address internal proxy;
|
||||
+ address internal implementation;
|
||||
+ BankV1 internal bank;
|
||||
+
|
||||
+ function setUp() public virtual {
|
||||
+ owner = makeAddr("owner");
|
||||
+ alice = makeAddr("alice");
|
||||
+ bob = makeAddr("bob");
|
||||
+ stranger = makeAddr("stranger");
|
||||
+
|
||||
+ token = new MockUSDC(owner);
|
||||
+ proxy =
|
||||
+ Upgrades.deployUUPSProxy("BankV1.sol:BankV1", abi.encodeCall(BankV1.initialize, (address(token), owner)));
|
||||
+ bank = BankV1(proxy);
|
||||
+ implementation = Upgrades.getImplementationAddress(proxy);
|
||||
+ }
|
||||
+}
|
||||
Reference in New Issue
Block a user