feat: establish UUPS bank V1
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
- State-changing scripts accept only chain IDs `31337` and `84532`; no mainnet RPC, address, target, or configuration is added.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- Generated Foundry output, local manifests, copied web artifacts, `.demo/` process state, `.env`, and `.superpowers/` are ignored by Git.
|
||||||
@@ -374,7 +374,7 @@ Expected red: `BankV1` is missing.
|
|||||||
```solidity
|
```solidity
|
||||||
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
|
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
|
||||||
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.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 {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
||||||
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
|
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
|
||||||
|
|
||||||
@@ -383,7 +383,7 @@ contract BankV1 is
|
|||||||
UUPSUpgradeable,
|
UUPSUpgradeable,
|
||||||
OwnableUpgradeable,
|
OwnableUpgradeable,
|
||||||
PausableUpgradeable,
|
PausableUpgradeable,
|
||||||
ReentrancyGuard
|
ReentrancyGuardTransient
|
||||||
{
|
{
|
||||||
IERC20 internal _asset;
|
IERC20 internal _asset;
|
||||||
mapping(address account => uint256 balance) internal _balances;
|
mapping(address account => uint256 balance) internal _balances;
|
||||||
@@ -392,7 +392,7 @@ contract BankV1 is
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
- [ ] Add the only permitted validator annotation and no other bypass:
|
||||||
|
|
||||||
@@ -1081,7 +1081,7 @@ Retain the explicit `deploy-v1`, `seed-v1`, and `sync-artifacts` lower-level tar
|
|||||||
|
|
||||||
- [ ] 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.
|
- [ ] 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:
|
- [ ] Run process and full verification:
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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