Files
uupl-smart-contract/.superpowers/sdd/2026-08-17-uups-bank-demo/task-3-report.md
T
golemandClaude Opus 5 fa36215def docs: track superpowers working documents in git
The uups-bank-demo wave's SDD records (ledger, six task briefs and reports,
review diffs) and the brainstorm design mockups were git-ignored, so they
existed only on one sandbox VM and reached no remote — this repo had no remote
at all until now.

Removes `.superpowers/` from .gitignore and the `*` .gitignore the superpowers
plugin writes inside .superpowers/sdd/; the second blocks the directory even
with the first removed.

Excluded as ephemeral local-server state, and now ignored by name:
.last-port, .last-token (a 64-char session token for a brainstorm server on a
port that is long gone), and the per-session state/ directories.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fnwzj6McD6kSkXwjUKFKxe
2026-08-20 14:38:00 -06:00

8.9 KiB

Task 3 Report: V1 proxy, initialization, views, and administration

Status

Complete. BankV1 now provides the initialized UUPS proxy shell, frozen V1 application storage, administrative pause controls, read-only accounting views, version reporting, and owner-gated upgrade authorization. The final implementation uses OpenZeppelin 5.6.1 ReentrancyGuardTransient, as explicitly selected by the user after the pinned validator incompatibility investigation described below.

Implementation

  • Added BankV1 with inheritance in the required order: Initializable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable, then the approved constructor-free ReentrancyGuardTransient.
  • Kept the V1 application fields in the frozen order: _asset, _balances, _totalLiabilities, then uint256[47] __gap.
  • Disabled initialization on the implementation constructor with the sole validator annotation @custom:oz-upgrades-unsafe-allow constructor.
  • Added initialize(address,address), checking the asset before invoking only __Ownable_init(initialOwner) and __Pausable_init().
  • Added owner-only pause() and unpause().
  • Added asset(), balanceOf(address), totalLiabilities(), and virtual pure contractVersion() returning literal 1.
  • Added owner-gated _authorizeUpgrade(address).
  • Added a reusable proxy fixture with deterministic owner, alice, bob, and stranger addresses, a MockUSDC, Upgrades.deployUUPSProxy, proxy-bound bank, and implementation address retained only for assertions.
  • Added 12 behavior tests covering successful initialization, validation order, disabled/double initialization, ownership and pause administration, paused views, upgrade authorization, and direct-versus-proxy UUPS context.
  • Updated the implementation plan's global, Task 3, scanner, and source-guard references to record the explicitly approved transient-guard design and restore the single ordinary constructor annotation.

Files

  • src/BankV1.sol — V1 UUPS implementation shell.
  • test/helpers/BankTestBase.sol — reusable proxy deployment fixture.
  • test/BankV1Admin.t.sol — initialization, administration, views, and UUPS-context tests.
  • docs/superpowers/plans/2026-08-17-uups-bank-demo.md — narrowly authorized compatibility/design update.
  • .superpowers/sdd/2026-08-17-uups-bank-demo/task-3-report.md — this ignored task report; not part of the commit.

TDD RED evidence

The required tool binaries were placed on PATH in a separate persistent-shell setup. The literal RED command was then issued exactly as briefed:

npm_config_offline=true forge test --match-path test/BankV1Admin.t.sol -vvv --force

Relevant output:

Compiler run failed:
Error (6275): Source "src/BankV1.sol" not found: File not found.
 --> test/BankV1Admin.t.sol:9:1
Error (6275): Source "src/BankV1.sol" not found: File not found.
 --> test/helpers/BankTestBase.sol:7:1
Error: Compilation failed

This was the expected RED: the tests and fixture referenced the required production API before BankV1 existed, and failed specifically because the feature was missing.

Validator incompatibility investigation and rulings

The first literal GREEN attempt used the brief's original non-upgradeable ReentrancyGuard and sole local @custom:oz-upgrades-unsafe-allow constructor. Solidity compiled, but Foundry Upgrades validation rejected the inherited dependency:

Upgrade safety validation failed:
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol:58:
Contract `ReentrancyGuard` has a constructor

Investigation established:

  • Pinned OpenZeppelin Contracts is 5.6.1 and marks ReentrancyGuard @custom:stateless, but that guard still has a constructor that initializes its namespaced guard slot.
  • Pinned upgrades-core is 1.46.0 and contains no support for the @custom:stateless annotation.
  • upgrades-core aggregates constructor errors from inherited contracts.

The first user ruling approved replacing the local annotation with @custom:oz-upgrades-unsafe-allow-reachable constructor. The exact change was made and the literal focused command rerun, but validation failed identically. Source-level tracing then established that upgrades-core 1.46.0 uses skipCheckReachable only for opcode traversal (delegatecall/selfdestruct); getConstructorErrors checks annotations lexically on each constructor node, so an annotation on BankV1 cannot suppress an inherited constructor finding.

No Options.unsafeAllow, exclude, skip, UnsafeUpgrades, dependency edit, or other workaround was applied. After that evidence, the user's final ruling selected OpenZeppelin 5.6.1 ReentrancyGuardTransient and restored the ordinary sole constructor annotation. This guard has no constructor or persistent application storage and is appropriate for this project's explicitly Cancun-targeted Anvil and Base Sepolia networks, where EIP-1153 is available.

One subsequent test-harness issue was also corrected: vm.expectRevert placed before Upgrades.deployUUPSProxy intercepted the library's preliminary implementation deployment. The invalid-initializer tests now construct a real ERC1967Proxy against the already validated implementation, making the next external operation the intended initializer delegatecall and preserving exact custom-error assertions.

GREEN and verification evidence

The brief's literal GREEN sequence was run in the same shell after the separate PATH setup:

forge fmt
npm_config_offline=true forge test --match-path test/BankV1Admin.t.sol -vvv --force
forge inspect BankV1 storage-layout

Final focused result:

Ran 12 tests for test/BankV1Admin.t.sol:BankV1AdminTest
Suite result: ok. 12 passed; 0 failed; 0 skipped

Storage inspection result:

_asset             IERC20                      slot 0  offset 0
_balances          mapping(address => uint256) slot 1  offset 0
_totalLiabilities uint256                     slot 2  offset 0
__gap              uint256[47]                 slot 3  offset 0 (1504 bytes)

The final full validation-bearing suite was run offline:

npm_config_offline=true forge test -vvv --force

Result:

Ran 2 test suites: 19 tests passed, 0 failed, 0 skipped

Both focused and full commands compiled successfully with only two existing dependency warnings in openzeppelin-foundry-upgrades about functions whose mutability could be pure; no project warning or error was emitted.

Storage and upgrade safety notes

  • Application storage is exactly _asset at slot 0, _balances at slot 1, _totalLiabilities at slot 2, and the 47-word gap beginning at slot 3. Later versions must not reorder, remove, or change the type of these fields.
  • OpenZeppelin ownership and pause state use ERC-7201 namespaced storage. UUPS, Initializable, and the approved transient reentrancy guard do not consume ordinary application slots; this does not permit reordering the frozen application fields.
  • Every ordinary bank call in the fixture and tests targets BankV1(proxy). The implementation address is used only to assert disabled initialization and direct proxiableUUID() behavior, as required.
  • proxiableUUID() returns the literal ERC-1967 implementation slot on the implementation and rejects proxy-context calls with UUPSUnauthorizedCallContext.
  • _authorizeUpgrade is covered through the public proxy upgradeToAndCall boundary and rejects a non-owner with OwnableUnauthorizedAccount.
  • The transient guard restricts supported execution environments to EIP-1153-capable networks. That is an explicit design constraint, not a hidden fallback.

Self-review

  • Reviewed all four committed diffs and ran git diff --check; no whitespace errors were found.
  • Confirmed the fixture uses the exact Upgrades.deployUUPSProxy("BankV1.sol:BankV1", abi.encodeCall(...)) form and binds all application behavior to the proxy.
  • Confirmed only one source validator annotation exists and no unsafeSkip, UnsafeUpgrades, Options unsafeAllow, exclude, reachable annotation, or dependency edit exists in Task 3 code/tests.
  • Strengthened the zero-asset test to pass both zero asset and zero owner, so it proves the mandated asset-check ordering; a mutation that invokes ownable initialization first now fails that test.
  • Mentally checked realistic mutations: wrong/missing asset assignment, wrong version, missing owner initialization, missing pause authorization/state change, pausing views, enabled reinitialization, implementation initialization, missing upgrade authorization, or broken UUPS call context each fail at least one test.
  • No deposit or withdrawal behavior was added.

Concerns

  • ReentrancyGuardTransient requires EIP-1153/Cancun. The approved project targets (Anvil with evm_version = "cancun" and Base Sepolia) satisfy this, but deploying this implementation on a pre-Cancun EVM is unsupported.
  • Existing dependency-only compiler mutability warnings remain; they do not originate from Task 3 files.