feat: add guarded Base Sepolia encore
This commit is contained in:
+11
-1
@@ -1 +1,11 @@
|
|||||||
# Copy this file to .env for local-only configuration.
|
# Copy this file to .env for local-only public configuration.
|
||||||
|
# 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=
|
||||||
|
BASE_SEPOLIA_SENDER=
|
||||||
|
BASE_SEPOLIA_RECIPIENT=
|
||||||
|
|
||||||
|
# Foundry prompts interactively for the named keystore password. Never store a
|
||||||
|
# signing key, mnemonic, or keystore password in .env.
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
.SHELLFLAGS := -euo pipefail -c
|
.SHELLFLAGS := -euo pipefail -c
|
||||||
|
|
||||||
|
-include .env
|
||||||
|
export BASE_SEPOLIA_RPC_URL BASE_SEPOLIA_PUBLIC_RPC_URL BASE_SEPOLIA_ACCOUNT BASE_SEPOLIA_SENDER BASE_SEPOLIA_RECIPIENT
|
||||||
|
|
||||||
RPC_LOCAL := http://127.0.0.1:8545
|
RPC_LOCAL := http://127.0.0.1:8545
|
||||||
ANVIL_OWNER := 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
|
ANVIL_OWNER := 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
|
||||||
ANVIL_ALICE := 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
|
ANVIL_ALICE := 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
|
||||||
|
|
||||||
.PHONY: doctor setup demo-local verify check-state reset-local deploy-v1 seed-v1 upgrade-v2 demo-transfer sync-artifacts sync-artifacts-check sync-abis publish-web-manifest test-finalize-manifest
|
.PHONY: doctor setup demo-local verify check-state reset-local deploy-v1 seed-v1 upgrade-v2 demo-transfer sync-artifacts sync-artifacts-check sync-abis publish-web-manifest test-finalize-manifest deploy-base-sepolia upgrade-base-sepolia transfer-base-sepolia select-anvil select-base-sepolia archive-base-manifest
|
||||||
doctor:
|
doctor:
|
||||||
@bash tools/doctor.sh
|
@bash tools/doctor.sh
|
||||||
setup:
|
setup:
|
||||||
@@ -23,6 +26,7 @@ verify:
|
|||||||
@node tools/test-finalize-manifest.mjs
|
@node tools/test-finalize-manifest.mjs
|
||||||
@node tools/test-sync-web-artifacts.mjs
|
@node tools/test-sync-web-artifacts.mjs
|
||||||
@bash tools/test-process-safety.sh
|
@bash tools/test-process-safety.sh
|
||||||
|
@bash tools/test-base-config.sh
|
||||||
@bash tools/test-scan-project.sh
|
@bash tools/test-scan-project.sh
|
||||||
@bash tools/scan-project.sh
|
@bash tools/scan-project.sh
|
||||||
@npm --prefix web run lint
|
@npm --prefix web run lint
|
||||||
@@ -63,3 +67,33 @@ demo-transfer:
|
|||||||
@DEMO_EXPECTED_STAGE=v2 $(MAKE) check-state
|
@DEMO_EXPECTED_STAGE=v2 $(MAKE) check-state
|
||||||
check-state:
|
check-state:
|
||||||
@forge script script/CheckState.s.sol:CheckState --rpc-url $(RPC_LOCAL) --force
|
@forge script script/CheckState.s.sol:CheckState --rpc-url $(RPC_LOCAL) --force
|
||||||
|
select-anvil:
|
||||||
|
@node tools/select-manifest.mjs anvil
|
||||||
|
@node tools/publish-web-manifest.mjs
|
||||||
|
select-base-sepolia:
|
||||||
|
@node tools/select-manifest.mjs baseSepolia
|
||||||
|
@node tools/publish-web-manifest.mjs
|
||||||
|
archive-base-manifest:
|
||||||
|
@node tools/select-manifest.mjs archive baseSepolia
|
||||||
|
deploy-base-sepolia:
|
||||||
|
@./tools/require-base-config.sh deploy
|
||||||
|
@node tools/finalize-manifest.mjs preflight-deploy baseSepolia
|
||||||
|
@SCRIPT_SENDER="$(BASE_SEPOLIA_SENDER)" DEPLOYMENT_MANIFEST_PATH=deployments/pending.json npm_config_offline=true forge script script/DeployV1.s.sol:DeployV1 --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --account "$(BASE_SEPOLIA_ACCOUNT)" --sender "$(BASE_SEPOLIA_SENDER)" --broadcast --slow --force
|
||||||
|
@DEPLOYMENT_MANIFEST_PATH=deployments/pending.json DEMO_EXPECTED_STAGE=deployed forge script script/CheckState.s.sol:CheckState --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --force
|
||||||
|
@node tools/finalize-manifest.mjs deploy --rpc-url "$(BASE_SEPOLIA_RPC_URL)"
|
||||||
|
@SCRIPT_SENDER="$(BASE_SEPOLIA_SENDER)" DEPLOYMENT_MANIFEST_PATH=deployments/base-sepolia.json forge script script/SeedBaseSepolia.s.sol:SeedBaseSepolia --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --account "$(BASE_SEPOLIA_ACCOUNT)" --sender "$(BASE_SEPOLIA_SENDER)" --broadcast --slow --force
|
||||||
|
@DEPLOYMENT_MANIFEST_PATH=deployments/base-sepolia.json DEMO_EXPECTED_STAGE=invariants forge script script/CheckState.s.sol:CheckState --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --force
|
||||||
|
@node tools/select-manifest.mjs baseSepolia
|
||||||
|
@node tools/sync-web-artifacts.mjs
|
||||||
|
@node tools/publish-web-manifest.mjs
|
||||||
|
upgrade-base-sepolia:
|
||||||
|
@./tools/require-base-config.sh upgrade
|
||||||
|
@SCRIPT_SENDER="$(BASE_SEPOLIA_SENDER)" DEPLOYMENT_MANIFEST_PATH=deployments/upgrade-pending.json npm_config_offline=true forge script script/UpgradeV2.s.sol:UpgradeV2 --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --account "$(BASE_SEPOLIA_ACCOUNT)" --sender "$(BASE_SEPOLIA_SENDER)" --broadcast --slow --force
|
||||||
|
@node tools/finalize-manifest.mjs upgrade --rpc-url "$(BASE_SEPOLIA_RPC_URL)"
|
||||||
|
@DEMO_EXPECTED_STAGE=invariants forge script script/CheckState.s.sol:CheckState --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --force
|
||||||
|
@node tools/sync-web-artifacts.mjs
|
||||||
|
@node tools/publish-web-manifest.mjs
|
||||||
|
transfer-base-sepolia:
|
||||||
|
@./tools/require-base-config.sh transfer
|
||||||
|
@SCRIPT_SENDER="$(BASE_SEPOLIA_SENDER)" forge script script/TransferV2Demo.s.sol:TransferV2Demo --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --account "$(BASE_SEPOLIA_ACCOUNT)" --sender "$(BASE_SEPOLIA_SENDER)" --broadcast --slow --force
|
||||||
|
@DEMO_EXPECTED_STAGE=invariants forge script script/CheckState.s.sol:CheckState --rpc-url "$(BASE_SEPOLIA_RPC_URL)" --force
|
||||||
|
|||||||
@@ -51,4 +51,12 @@ Foundry scripts are the state-changing control plane; the browser never signs. `
|
|||||||
- `make sync-artifacts` — regenerate the ABI module and publish the confirmed active manifest.
|
- `make sync-artifacts` — regenerate the ABI module and publish the confirmed active manifest.
|
||||||
- `make sync-artifacts-check` — test generation and prove generated ABIs are current.
|
- `make sync-artifacts-check` — test generation and prove generated ABIs are current.
|
||||||
|
|
||||||
|
## Optional Base Sepolia encore
|
||||||
|
|
||||||
|
The local lesson is complete without a wallet, faucet, explorer, or public RPC. An optional Base Sepolia encore is available only after the local V1→V2 demo and `make verify` succeed. It uses a named encrypted Foundry keystore through `--account` plus the same public address through `--sender`; there is no private-key or mnemonic fallback.
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and fill only its public configuration. `BASE_SEPOLIA_RPC_URL` is the terminal endpoint and may be credentialed; `BASE_SEPOLIA_PUBLIC_RPC_URL` is intentionally public and is the only RPC serialized for the browser. Foundry requests the keystore password interactively, and the password never belongs in `.env`.
|
||||||
|
|
||||||
|
Use `make archive-base-manifest` before an intentional Base redeployment. The command moves only the Base canonical manifest to a timestamped sibling; it preserves the active browser fallback and all Anvil state. `make select-anvil` and `make select-base-sepolia` explicitly switch the active manifest and republish the browser copy. See the presenter runbook for the exact wallet onboarding, funding boundary, deploy/upgrade/transfer sequence, and recovery rules.
|
||||||
|
|
||||||
Continue with the [learning guide](docs/LEARNING_GUIDE.md) or rehearse from the [presenter runbook](docs/PRESENTER_RUNBOOK.md).
|
Continue with the [learning guide](docs/LEARNING_GUIDE.md) or rehearse from the [presenter runbook](docs/PRESENTER_RUNBOOK.md).
|
||||||
|
|||||||
@@ -74,6 +74,49 @@ The expected result is the same proxy and a new implementation, version `2`, Ali
|
|||||||
|
|
||||||
End the local session with Ctrl-C in the attached terminal, then `make reset-local`. The optional public encore is outside this prepared V1 run: local success does not depend on Base Sepolia, a faucet, an explorer, a wallet, or any external RPC.
|
End the local session with Ctrl-C in the attached terminal, then `make reset-local`. The optional public encore is outside this prepared V1 run: local success does not depend on Base Sepolia, a faucet, an explorer, a wallet, or any external RPC.
|
||||||
|
|
||||||
|
## 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 verify the two refer to the same account. Choose a different nonzero public address for `BASE_SEPOLIA_RECIPIENT`. Fund only the displayed Base Sepolia sender address with Base Sepolia test ETH. Never paste the key or password into Codex, shell history, or `.env`; Foundry requests the encrypted-keystore password through its interactive prompt.
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and configure:
|
||||||
|
|
||||||
|
```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 must not contain a raw-key option. The URLs above are deliberately fake; these checks require no keystore, faucet, or network. Then, only with explicit authorization and Base Sepolia test funds, execute:
|
||||||
|
|
||||||
|
```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 transfer produces Presenter `750 mUSDC` and Recipient `250 mUSDC`, while reserves and liabilities stay `1,000 mUSDC`. Each command runs an invariant-only state check. The confirmed browser manifest contains only the intentionally public browser RPC plus `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. If a faucet, RPC, or explorer fails, record the optional failure and stop—the completed local demo remains the successful outcome.
|
||||||
|
|
||||||
## Closing trust disclosure checklist
|
## Closing trust disclosure checklist
|
||||||
|
|
||||||
Read these points while the matching console panel is visible:
|
Read these points while the matching console panel is visible:
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ contract CheckState is DemoScript {
|
|||||||
_printEducationalWarning();
|
_printEducationalWarning();
|
||||||
bool deployedStage = keccak256(bytes(stage)) == keccak256("deployed");
|
bool deployedStage = keccak256(bytes(stage)) == keccak256("deployed");
|
||||||
Manifest memory manifest = _readManifest(manifestPath, !deployedStage);
|
Manifest memory manifest = _readManifest(manifestPath, !deployedStage);
|
||||||
|
if (block.chainid == BASE_SEPOLIA_CHAIN_ID) {
|
||||||
|
address expectedSender = vm.envAddress("BASE_SEPOLIA_SENDER");
|
||||||
|
(address presenter, address recipient,) = _requireBaseConfig(expectedSender);
|
||||||
|
_assertAddress("Presenter", manifest.actors[0].address_, presenter);
|
||||||
|
_assertAddress("Recipient", manifest.actors[1].address_, recipient);
|
||||||
|
}
|
||||||
BankV1 bank = BankV1(manifest.proxy);
|
BankV1 bank = BankV1(manifest.proxy);
|
||||||
MockUSDC token = MockUSDC(manifest.token);
|
MockUSDC token = MockUSDC(manifest.token);
|
||||||
|
|
||||||
|
|||||||
+13
-5
@@ -11,6 +11,12 @@ contract DeployV1 is DemoScript {
|
|||||||
_requireSupportedChain(block.chainid);
|
_requireSupportedChain(block.chainid);
|
||||||
_printEducationalWarning();
|
_printEducationalWarning();
|
||||||
address sender = vm.envAddress("SCRIPT_SENDER");
|
address sender = vm.envAddress("SCRIPT_SENDER");
|
||||||
|
address recipient;
|
||||||
|
string memory publicRpcUrl;
|
||||||
|
|
||||||
|
if (block.chainid == BASE_SEPOLIA_CHAIN_ID) {
|
||||||
|
(, recipient, publicRpcUrl) = _requireBaseConfig(sender);
|
||||||
|
}
|
||||||
|
|
||||||
if (block.chainid == ANVIL_CHAIN_ID) {
|
if (block.chainid == ANVIL_CHAIN_ID) {
|
||||||
(uint256 ownerKey, address derivedOwner) = _deriveLocalActor(block.chainid, 0);
|
(uint256 ownerKey, address derivedOwner) = _deriveLocalActor(block.chainid, 0);
|
||||||
@@ -40,17 +46,18 @@ contract DeployV1 is DemoScript {
|
|||||||
manifest.network = block.chainid == ANVIL_CHAIN_ID ? "anvil" : "baseSepolia";
|
manifest.network = block.chainid == ANVIL_CHAIN_ID ? "anvil" : "baseSepolia";
|
||||||
manifest.chainId = block.chainid;
|
manifest.chainId = block.chainid;
|
||||||
manifest.deploymentBlock = 0;
|
manifest.deploymentBlock = 0;
|
||||||
manifest.rpcUrl = block.chainid == ANVIL_CHAIN_ID ? "http://127.0.0.1:8545" : "";
|
manifest.rpcUrl = block.chainid == ANVIL_CHAIN_ID ? "http://127.0.0.1:8545" : publicRpcUrl;
|
||||||
|
manifest.explorerBaseUrl = block.chainid == BASE_SEPOLIA_CHAIN_ID ? "https://sepolia.basescan.org" : "";
|
||||||
manifest.token = tokenAddress;
|
manifest.token = tokenAddress;
|
||||||
manifest.proxy = proxy;
|
manifest.proxy = proxy;
|
||||||
manifest.implementation = implementation;
|
manifest.implementation = implementation;
|
||||||
manifest.owner = sender;
|
manifest.owner = sender;
|
||||||
manifest.actors = _actors(sender);
|
manifest.actors = _actors(sender, recipient);
|
||||||
|
|
||||||
_writeManifest(_manifestPath(PENDING_MANIFEST_PATH), manifest);
|
_writeManifest(_manifestPath(PENDING_MANIFEST_PATH), manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _actors(address sender) private view returns (Actor[] memory actors) {
|
function _actors(address sender, address recipient) private view returns (Actor[] memory actors) {
|
||||||
if (block.chainid == ANVIL_CHAIN_ID) {
|
if (block.chainid == ANVIL_CHAIN_ID) {
|
||||||
actors = new Actor[](3);
|
actors = new Actor[](3);
|
||||||
actors[0] = Actor({label: "owner", address_: sender});
|
actors[0] = Actor({label: "owner", address_: sender});
|
||||||
@@ -59,8 +66,9 @@ contract DeployV1 is DemoScript {
|
|||||||
actors[1].label = "Alice";
|
actors[1].label = "Alice";
|
||||||
actors[2].label = "Bob";
|
actors[2].label = "Bob";
|
||||||
} else {
|
} else {
|
||||||
actors = new Actor[](1);
|
actors = new Actor[](2);
|
||||||
actors[0] = Actor({label: "owner", address_: sender});
|
actors[0] = Actor({label: "Presenter", address_: sender});
|
||||||
|
actors[1] = Actor({label: "Recipient", address_: recipient});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.35;
|
||||||
|
|
||||||
|
import {BankV1} from "../src/BankV1.sol";
|
||||||
|
import {MockUSDC} from "../src/MockUSDC.sol";
|
||||||
|
import {DemoScript} from "./lib/DemoScript.sol";
|
||||||
|
|
||||||
|
contract SeedBaseSepolia is DemoScript {
|
||||||
|
uint256 internal constant SEED_AMOUNT = 1_000e6;
|
||||||
|
|
||||||
|
function run() external {
|
||||||
|
if (block.chainid != BASE_SEPOLIA_CHAIN_ID) revert UnsupportedChain(block.chainid);
|
||||||
|
_printEducationalWarning();
|
||||||
|
address scriptSender = vm.envAddress("SCRIPT_SENDER");
|
||||||
|
(address presenter, address recipient,) = _requireBaseConfig(scriptSender);
|
||||||
|
Manifest memory manifest = _readManifest(_manifestPath(ACTIVE_MANIFEST_PATH), true);
|
||||||
|
_assertAddress("Presenter", manifest.actors[0].address_, presenter);
|
||||||
|
_assertAddress("Recipient", manifest.actors[1].address_, recipient);
|
||||||
|
|
||||||
|
BankV1 bank = BankV1(manifest.proxy);
|
||||||
|
MockUSDC token = MockUSDC(manifest.token);
|
||||||
|
_assertDeployedState(manifest);
|
||||||
|
|
||||||
|
vm.startBroadcast(presenter);
|
||||||
|
token.mint(presenter, SEED_AMOUNT);
|
||||||
|
token.approve(manifest.proxy, SEED_AMOUNT);
|
||||||
|
bank.deposit(SEED_AMOUNT);
|
||||||
|
vm.stopBroadcast();
|
||||||
|
|
||||||
|
_assertUint("Presenter internal balance", SEED_AMOUNT, bank.balanceOf(presenter));
|
||||||
|
_assertUint("Recipient internal balance", 0, bank.balanceOf(recipient));
|
||||||
|
_assertUint("liabilities", SEED_AMOUNT, bank.totalLiabilities());
|
||||||
|
_assertUint("reserves", SEED_AMOUNT, token.balanceOf(manifest.proxy));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,18 @@ contract TransferV2Demo is DemoScript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _run(string memory manifestPath) internal {
|
function _run(string memory manifestPath) internal {
|
||||||
if (block.chainid != ANVIL_CHAIN_ID) revert UnsupportedChain(block.chainid);
|
_requireSupportedChain(block.chainid);
|
||||||
_printEducationalWarning();
|
_printEducationalWarning();
|
||||||
Manifest memory manifest = _readManifest(manifestPath, true);
|
Manifest memory manifest = _readManifest(manifestPath, true);
|
||||||
|
if (block.chainid == BASE_SEPOLIA_CHAIN_ID) {
|
||||||
|
_runBase(manifest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_runLocal(manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _runLocal(Manifest memory manifest) private {
|
||||||
(uint256 aliceKey, address alice) = _deriveLocalActor(block.chainid, 1);
|
(uint256 aliceKey, address alice) = _deriveLocalActor(block.chainid, 1);
|
||||||
address bob = manifest.actors[2].address_;
|
address bob = manifest.actors[2].address_;
|
||||||
_assertAddress("Alice actor", manifest.actors[1].address_, alice);
|
_assertAddress("Alice actor", manifest.actors[1].address_, alice);
|
||||||
@@ -38,4 +47,31 @@ contract TransferV2Demo is DemoScript {
|
|||||||
_assertUint("reserves", reserves, token.balanceOf(manifest.proxy));
|
_assertUint("reserves", reserves, token.balanceOf(manifest.proxy));
|
||||||
_assertUint("surplus", 0, token.balanceOf(manifest.proxy) - bank.totalLiabilities());
|
_assertUint("surplus", 0, token.balanceOf(manifest.proxy) - bank.totalLiabilities());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _runBase(Manifest memory manifest) private {
|
||||||
|
address scriptSender = vm.envAddress("SCRIPT_SENDER");
|
||||||
|
(address presenter, address recipient,) = _requireBaseConfig(scriptSender);
|
||||||
|
_assertAddress("Presenter", manifest.actors[0].address_, presenter);
|
||||||
|
_assertAddress("Recipient", manifest.actors[1].address_, recipient);
|
||||||
|
|
||||||
|
BankV2 bank = BankV2(manifest.proxy);
|
||||||
|
MockUSDC token = MockUSDC(manifest.token);
|
||||||
|
_assertUint("version", 2, bank.contractVersion());
|
||||||
|
_assertUint("Presenter internal balance", 1_000e6, bank.balanceOf(presenter));
|
||||||
|
_assertUint("Recipient internal balance", 0, bank.balanceOf(recipient));
|
||||||
|
uint256 liabilities = bank.totalLiabilities();
|
||||||
|
uint256 reserves = token.balanceOf(manifest.proxy);
|
||||||
|
_assertUint("liabilities", 1_000e6, liabilities);
|
||||||
|
_assertUint("reserves", 1_000e6, reserves);
|
||||||
|
|
||||||
|
vm.startBroadcast(presenter);
|
||||||
|
bank.transferBalance(recipient, 250e6);
|
||||||
|
vm.stopBroadcast();
|
||||||
|
|
||||||
|
_assertUint("Presenter internal balance", 750e6, bank.balanceOf(presenter));
|
||||||
|
_assertUint("Recipient internal balance", 250e6, bank.balanceOf(recipient));
|
||||||
|
_assertUint("liabilities", liabilities, bank.totalLiabilities());
|
||||||
|
_assertUint("reserves", reserves, token.balanceOf(manifest.proxy));
|
||||||
|
_assertUint("surplus", 0, token.balanceOf(manifest.proxy) - bank.totalLiabilities());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,15 @@ contract UpgradeV2 is DemoScript {
|
|||||||
internal
|
internal
|
||||||
returns (bool upgraded, address implementation)
|
returns (bool upgraded, address implementation)
|
||||||
{
|
{
|
||||||
|
address configuredRecipient;
|
||||||
|
if (block.chainid == BASE_SEPOLIA_CHAIN_ID) {
|
||||||
|
(, configuredRecipient,) = _requireBaseConfig(sender);
|
||||||
|
}
|
||||||
Manifest memory manifest = _readManifest(activePath, true);
|
Manifest memory manifest = _readManifest(activePath, true);
|
||||||
|
if (block.chainid == BASE_SEPOLIA_CHAIN_ID) {
|
||||||
|
_assertAddress("Presenter", manifest.actors[0].address_, sender);
|
||||||
|
_assertAddress("Recipient", manifest.actors[1].address_, configuredRecipient);
|
||||||
|
}
|
||||||
BankV1 bank = BankV1(manifest.proxy);
|
BankV1 bank = BankV1(manifest.proxy);
|
||||||
|
|
||||||
address actualImplementation = Upgrades.getImplementationAddress(manifest.proxy);
|
address actualImplementation = Upgrades.getImplementationAddress(manifest.proxy);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ abstract contract DemoScript is Script {
|
|||||||
error InvalidDeploymentBlock();
|
error InvalidDeploymentBlock();
|
||||||
error InvalidManifestSchema(uint256 schemaVersion);
|
error InvalidManifestSchema(uint256 schemaVersion);
|
||||||
error InvalidActorConfiguration();
|
error InvalidActorConfiguration();
|
||||||
|
error InvalidPublicRpcUrl();
|
||||||
error UnexpectedState(string label, uint256 expected, uint256 actual);
|
error UnexpectedState(string label, uint256 expected, uint256 actual);
|
||||||
error UnexpectedAddress(string label, address expected, address actual);
|
error UnexpectedAddress(string label, address expected, address actual);
|
||||||
|
|
||||||
@@ -60,6 +61,72 @@ abstract contract DemoScript is Script {
|
|||||||
return vm.envOr("DEPLOYMENT_MANIFEST_PATH", defaultPath);
|
return vm.envOr("DEPLOYMENT_MANIFEST_PATH", defaultPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _requireBaseSender(address scriptSender) internal view returns (address sender) {
|
||||||
|
sender = vm.envAddress("BASE_SEPOLIA_SENDER");
|
||||||
|
_assertAddress("BASE_SEPOLIA_SENDER", scriptSender, sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _requireBaseConfig(address scriptSender)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (address sender, address recipient, string memory publicRpcUrl)
|
||||||
|
{
|
||||||
|
sender = _requireBaseSender(scriptSender);
|
||||||
|
recipient = vm.envAddress("BASE_SEPOLIA_RECIPIENT");
|
||||||
|
if (sender == address(0) || recipient == address(0) || sender == recipient) revert InvalidActorConfiguration();
|
||||||
|
publicRpcUrl = vm.envString("BASE_SEPOLIA_PUBLIC_RPC_URL");
|
||||||
|
_requirePublicHttpsUrl(publicRpcUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _requirePublicHttpsUrl(string memory value) internal pure {
|
||||||
|
bytes memory url = bytes(value);
|
||||||
|
bytes memory prefix = bytes("https://");
|
||||||
|
if (url.length <= prefix.length) revert InvalidPublicRpcUrl();
|
||||||
|
for (uint256 i; i < prefix.length; ++i) {
|
||||||
|
if (url[i] != prefix[i]) revert InvalidPublicRpcUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool query;
|
||||||
|
uint256 queryStart;
|
||||||
|
for (uint256 i = prefix.length; i < url.length; ++i) {
|
||||||
|
bytes1 character = url[i];
|
||||||
|
if (!query && character == "@") revert InvalidPublicRpcUrl();
|
||||||
|
if (character == "?") {
|
||||||
|
query = true;
|
||||||
|
queryStart = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (character == "#") break;
|
||||||
|
}
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
bytes memory lowered = new bytes(url.length - queryStart);
|
||||||
|
for (uint256 i; i < lowered.length; ++i) {
|
||||||
|
uint8 character = uint8(url[queryStart + i]);
|
||||||
|
lowered[i] = character >= 65 && character <= 90 ? bytes1(character + 32) : bytes1(character);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
_containsBytes(lowered, bytes("key=")) || _containsBytes(lowered, bytes("token="))
|
||||||
|
|| _containsBytes(lowered, bytes("secret=")) || _containsBytes(lowered, bytes("password="))
|
||||||
|
|| _containsBytes(lowered, bytes("credential="))
|
||||||
|
) revert InvalidPublicRpcUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _containsBytes(bytes memory haystack, bytes memory needle) private pure returns (bool) {
|
||||||
|
if (needle.length > haystack.length) return false;
|
||||||
|
for (uint256 i; i + needle.length <= haystack.length; ++i) {
|
||||||
|
bool matches = true;
|
||||||
|
for (uint256 j; j < needle.length; ++j) {
|
||||||
|
if (haystack[i + j] != needle[j]) {
|
||||||
|
matches = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function _readManifest(string memory path, bool active) internal view returns (Manifest memory manifest) {
|
function _readManifest(string memory path, bool active) internal view returns (Manifest memory manifest) {
|
||||||
string memory json = vm.readFile(path);
|
string memory json = vm.readFile(path);
|
||||||
_assertExactManifestSchema(json);
|
_assertExactManifestSchema(json);
|
||||||
@@ -112,7 +179,7 @@ abstract contract DemoScript is Script {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _parseActors(string memory json, uint256 chainId) private view returns (Actor[] memory actors) {
|
function _parseActors(string memory json, uint256 chainId) private view returns (Actor[] memory actors) {
|
||||||
uint256 actorCount = chainId == ANVIL_CHAIN_ID ? 3 : 1;
|
uint256 actorCount = chainId == ANVIL_CHAIN_ID ? 3 : 2;
|
||||||
actors = new Actor[](actorCount);
|
actors = new Actor[](actorCount);
|
||||||
for (uint256 i; i < actorCount; ++i) {
|
for (uint256 i; i < actorCount; ++i) {
|
||||||
string memory index = vm.toString(i);
|
string memory index = vm.toString(i);
|
||||||
@@ -157,9 +224,13 @@ abstract contract DemoScript is Script {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
manifest.chainId != BASE_SEPOLIA_CHAIN_ID || manifest.actors.length != 1
|
manifest.chainId != BASE_SEPOLIA_CHAIN_ID || manifest.actors.length != 2
|
||||||
|| !_equals(manifest.network, "baseSepolia") || !_equals(manifest.actors[0].label, "owner")
|
|| !_equals(manifest.network, "baseSepolia") || !_equals(manifest.actors[0].label, "Presenter")
|
||||||
|| manifest.actors[0].address_ != manifest.owner
|
|| !_equals(manifest.actors[1].label, "Recipient") || manifest.actors[0].address_ != manifest.owner
|
||||||
|
) revert InvalidActorConfiguration();
|
||||||
|
if (
|
||||||
|
manifest.actors[0].address_ == address(0) || manifest.actors[1].address_ == address(0)
|
||||||
|
|| manifest.actors[0].address_ == manifest.actors[1].address_
|
||||||
) revert InvalidActorConfiguration();
|
) revert InvalidActorConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+220
-8
@@ -6,6 +6,7 @@ import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
|
|||||||
import {DemoScript} from "../script/lib/DemoScript.sol";
|
import {DemoScript} from "../script/lib/DemoScript.sol";
|
||||||
import {DeployV1} from "../script/DeployV1.s.sol";
|
import {DeployV1} from "../script/DeployV1.s.sol";
|
||||||
import {SeedV1Demo} from "../script/SeedV1Demo.s.sol";
|
import {SeedV1Demo} from "../script/SeedV1Demo.s.sol";
|
||||||
|
import {SeedBaseSepolia} from "../script/SeedBaseSepolia.s.sol";
|
||||||
import {CheckState} from "../script/CheckState.s.sol";
|
import {CheckState} from "../script/CheckState.s.sol";
|
||||||
import {UpgradeV2} from "../script/UpgradeV2.s.sol";
|
import {UpgradeV2} from "../script/UpgradeV2.s.sol";
|
||||||
import {TransferV2Demo} from "../script/TransferV2Demo.s.sol";
|
import {TransferV2Demo} from "../script/TransferV2Demo.s.sol";
|
||||||
@@ -88,7 +89,8 @@ contract ScriptPreflightTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function testUnsupportedChainsAreRejectedBeforeBroadcast() public {
|
function testUnsupportedChainsAreRejectedBeforeBroadcast() public {
|
||||||
uint256[3] memory rejected = [uint256(1), uint256(8453), uint256(7777777)];
|
uint256[7] memory rejected =
|
||||||
|
[uint256(1), uint256(10), uint256(56), uint256(137), uint256(8453), uint256(42161), uint256(7777777)];
|
||||||
for (uint256 i; i < rejected.length; ++i) {
|
for (uint256 i; i < rejected.length; ++i) {
|
||||||
vm.expectRevert(abi.encodeWithSelector(DemoScript.UnsupportedChain.selector, rejected[i]));
|
vm.expectRevert(abi.encodeWithSelector(DemoScript.UnsupportedChain.selector, rejected[i]));
|
||||||
harness.requireSupportedChain(rejected[i]);
|
harness.requireSupportedChain(rejected[i]);
|
||||||
@@ -210,7 +212,7 @@ contract ScriptPreflightTest is Test {
|
|||||||
harness.readManifest(path, true);
|
harness.readManifest(path, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBaseManifestRequiresOnlyOwnerActor() public {
|
function testBaseManifestRequiresExactPresenterRecipientActors() public {
|
||||||
string memory path = _writePublicBaseManifest();
|
string memory path = _writePublicBaseManifest();
|
||||||
vm.chainId(84532);
|
vm.chainId(84532);
|
||||||
_etchManifestContracts();
|
_etchManifestContracts();
|
||||||
@@ -270,16 +272,18 @@ contract ScriptPreflightTest is Test {
|
|||||||
harness.readManifest(path, true);
|
harness.readManifest(path, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBaseSepoliaManifestUsesCamelCaseAndOmitsUnavailableUrls() public {
|
function testBaseSepoliaManifestUsesPublicUrlsAndExactActors() public {
|
||||||
string memory path = _writePublicBaseManifest();
|
string memory path = _writePublicBaseManifest();
|
||||||
vm.chainId(84532);
|
vm.chainId(84532);
|
||||||
_etchManifestContracts();
|
_etchManifestContracts();
|
||||||
DemoScript.Manifest memory manifest = harness.readManifest(path, true);
|
DemoScript.Manifest memory manifest = harness.readManifest(path, true);
|
||||||
assertEq(manifest.network, "baseSepolia");
|
assertEq(manifest.network, "baseSepolia");
|
||||||
assertEq(manifest.rpcUrl, "");
|
assertEq(manifest.rpcUrl, "https://public.invalid");
|
||||||
assertEq(manifest.explorerBaseUrl, "");
|
assertEq(manifest.explorerBaseUrl, "https://sepolia.basescan.org");
|
||||||
assertEq(manifest.actors[0].label, "owner");
|
assertEq(manifest.actors[0].label, "Presenter");
|
||||||
assertEq(manifest.actors[0].address_, OWNER);
|
assertEq(manifest.actors[0].address_, OWNER);
|
||||||
|
assertEq(manifest.actors[1].label, "Recipient");
|
||||||
|
assertEq(manifest.actors[1].address_, BOB);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSerializedManifestContainsPublicAddressesAndNoSecrets() public {
|
function testSerializedManifestContainsPublicAddressesAndNoSecrets() public {
|
||||||
@@ -332,6 +336,76 @@ contract ScriptPreflightTest is Test {
|
|||||||
assertEq(manifest.actors[2].address_, 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC);
|
assertEq(manifest.actors[2].address_, 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testBaseDeployRejectsMismatchedConfiguredSenderBeforeBroadcast() public {
|
||||||
|
string memory path = string.concat(fixtureDir, "/base-deploy-mismatch.json");
|
||||||
|
vm.chainId(84532);
|
||||||
|
vm.setEnv("SCRIPT_SENDER", vm.toString(OWNER));
|
||||||
|
vm.setEnv("BASE_SEPOLIA_SENDER", vm.toString(ALICE));
|
||||||
|
vm.setEnv("BASE_SEPOLIA_RECIPIENT", vm.toString(BOB));
|
||||||
|
vm.setEnv("BASE_SEPOLIA_PUBLIC_RPC_URL", "https://public.invalid");
|
||||||
|
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
|
||||||
|
uint256 nonceBefore = vm.getNonce(OWNER);
|
||||||
|
DeployV1 deployer = new DeployV1();
|
||||||
|
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "BASE_SEPOLIA_SENDER", OWNER, ALICE)
|
||||||
|
);
|
||||||
|
deployer.run();
|
||||||
|
|
||||||
|
assertEq(vm.getNonce(OWNER), nonceBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBaseDeployWritesPublicRpcExplorerAndPresenterRecipientActors() public {
|
||||||
|
string memory path = string.concat(fixtureDir, "/base-deployed.json");
|
||||||
|
vm.chainId(84532);
|
||||||
|
_setBaseConfig(OWNER, BOB, "https://public.invalid");
|
||||||
|
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
|
||||||
|
|
||||||
|
(address token, address proxy, address implementation) = new DeployV1().run();
|
||||||
|
|
||||||
|
_etchManifestContracts();
|
||||||
|
DemoScript.Manifest memory manifest = harness.readManifest(path, false);
|
||||||
|
assertEq(manifest.token, token);
|
||||||
|
assertEq(manifest.proxy, proxy);
|
||||||
|
assertEq(manifest.implementation, implementation);
|
||||||
|
assertEq(manifest.owner, OWNER);
|
||||||
|
assertEq(manifest.rpcUrl, "https://public.invalid");
|
||||||
|
assertEq(manifest.explorerBaseUrl, "https://sepolia.basescan.org");
|
||||||
|
assertEq(manifest.actors.length, 2);
|
||||||
|
assertEq(manifest.actors[0].label, "Presenter");
|
||||||
|
assertEq(manifest.actors[0].address_, OWNER);
|
||||||
|
assertEq(manifest.actors[1].label, "Recipient");
|
||||||
|
assertEq(manifest.actors[1].address_, BOB);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBaseDeployRejectsZeroOrSameRecipientBeforeBroadcast() public {
|
||||||
|
vm.chainId(84532);
|
||||||
|
DeployV1 deployer = new DeployV1();
|
||||||
|
address[2] memory invalidRecipients = [address(0), OWNER];
|
||||||
|
for (uint256 i; i < invalidRecipients.length; ++i) {
|
||||||
|
_setBaseConfig(OWNER, invalidRecipients[i], "https://public.invalid");
|
||||||
|
uint256 nonceBefore = vm.getNonce(OWNER);
|
||||||
|
vm.expectRevert(DemoScript.InvalidActorConfiguration.selector);
|
||||||
|
deployer.run();
|
||||||
|
assertEq(vm.getNonce(OWNER), nonceBefore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBaseDeployRejectsCredentialBearingPublicRpcBeforeBroadcast() public {
|
||||||
|
vm.chainId(84532);
|
||||||
|
DeployV1 deployer = new DeployV1();
|
||||||
|
string[3] memory invalidUrls =
|
||||||
|
["http://public.invalid", "https://user@public.invalid", "https://public.invalid/path?api_key=fixture"];
|
||||||
|
bytes4 invalidPublicRpcUrl = bytes4(keccak256("InvalidPublicRpcUrl()"));
|
||||||
|
for (uint256 i; i < invalidUrls.length; ++i) {
|
||||||
|
_setBaseConfig(OWNER, BOB, invalidUrls[i]);
|
||||||
|
uint256 nonceBefore = vm.getNonce(OWNER);
|
||||||
|
vm.expectRevert(invalidPublicRpcUrl);
|
||||||
|
deployer.run();
|
||||||
|
assertEq(vm.getNonce(OWNER), nonceBefore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function testSeedV1DemoExecutesExactActOneStateAndCheckStateAcceptsIt() public {
|
function testSeedV1DemoExecutesExactActOneStateAndCheckStateAcceptsIt() public {
|
||||||
(string memory path, DemoScript.Manifest memory manifest) = _deployFixtureNamed("seed-v1");
|
(string memory path, DemoScript.Manifest memory manifest) = _deployFixtureNamed("seed-v1");
|
||||||
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
|
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
|
||||||
@@ -350,6 +424,38 @@ contract ScriptPreflightTest is Test {
|
|||||||
new CheckState().run();
|
new CheckState().run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testSeedBaseSepoliaCreatesExactPresenterDepositState() public {
|
||||||
|
(string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("seed-base");
|
||||||
|
_setBaseConfig(OWNER, BOB, "https://public.invalid");
|
||||||
|
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
|
||||||
|
|
||||||
|
new SeedBaseSepolia().run();
|
||||||
|
|
||||||
|
BankV1 bank = BankV1(manifest.proxy);
|
||||||
|
MockUSDC token = MockUSDC(manifest.token);
|
||||||
|
assertEq(bank.balanceOf(OWNER), 1_000e6);
|
||||||
|
assertEq(bank.balanceOf(BOB), 0);
|
||||||
|
assertEq(bank.totalLiabilities(), 1_000e6);
|
||||||
|
assertEq(token.balanceOf(manifest.proxy), 1_000e6);
|
||||||
|
assertEq(token.balanceOf(OWNER), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSeedBaseSepoliaRejectsMismatchedSenderBeforeMintOrBroadcast() public {
|
||||||
|
(string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("seed-base-mismatch");
|
||||||
|
_setBaseConfig(OWNER, BOB, "https://public.invalid");
|
||||||
|
vm.setEnv("BASE_SEPOLIA_SENDER", vm.toString(ALICE));
|
||||||
|
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
|
||||||
|
SeedBaseSepolia seeder = new SeedBaseSepolia();
|
||||||
|
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "BASE_SEPOLIA_SENDER", OWNER, ALICE)
|
||||||
|
);
|
||||||
|
seeder.run();
|
||||||
|
|
||||||
|
assertEq(MockUSDC(manifest.token).totalSupply(), 0);
|
||||||
|
assertEq(BankV1(manifest.proxy).totalLiabilities(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
function testCheckStateRejectsManifestImplementationMismatch() public {
|
function testCheckStateRejectsManifestImplementationMismatch() public {
|
||||||
(string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("check-mismatch");
|
(string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("check-mismatch");
|
||||||
vm.writeJson(string.concat('"', vm.toString(manifest.token), '"'), path, ".implementation");
|
vm.writeJson(string.concat('"', vm.toString(manifest.token), '"'), path, ".implementation");
|
||||||
@@ -506,6 +612,73 @@ contract ScriptPreflightTest is Test {
|
|||||||
assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6);
|
assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testBaseUpgradeAndTransferPreserveAccountingAndMoveExactBalance() public {
|
||||||
|
(string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("transfer-base");
|
||||||
|
_setBaseConfig(OWNER, BOB, "https://public.invalid");
|
||||||
|
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
|
||||||
|
new SeedBaseSepolia().run();
|
||||||
|
|
||||||
|
string memory pending = string.concat(fixtureDir, "/base-upgrade-pending.json");
|
||||||
|
(, address implementation) = new UpgradeV2Harness().runWithPaths(path, pending, OWNER);
|
||||||
|
vm.writeJson(string.concat('"', vm.toString(implementation), '"'), path, ".implementation");
|
||||||
|
|
||||||
|
new TransferV2DemoHarness().runWithPath(path);
|
||||||
|
|
||||||
|
BankV2 bank = BankV2(manifest.proxy);
|
||||||
|
MockUSDC token = MockUSDC(manifest.token);
|
||||||
|
assertEq(bank.balanceOf(OWNER), 750e6);
|
||||||
|
assertEq(bank.balanceOf(BOB), 250e6);
|
||||||
|
assertEq(bank.totalLiabilities(), 1_000e6);
|
||||||
|
assertEq(token.balanceOf(manifest.proxy), 1_000e6);
|
||||||
|
new CheckStateHarness().runWithPath(path, "invariants");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBaseUpgradeRejectsMismatchedConfiguredSenderBeforeBroadcast() public {
|
||||||
|
(string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("upgrade-base-mismatch");
|
||||||
|
_setBaseConfig(ALICE, BOB, "https://public.invalid");
|
||||||
|
string memory pending = string.concat(fixtureDir, "/base-mismatch-pending.json");
|
||||||
|
UpgradeV2Harness upgrader = new UpgradeV2Harness();
|
||||||
|
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "BASE_SEPOLIA_SENDER", OWNER, ALICE)
|
||||||
|
);
|
||||||
|
upgrader.runWithPaths(path, pending, OWNER);
|
||||||
|
|
||||||
|
assertEq(Upgrades.getImplementationAddress(manifest.proxy), manifest.implementation);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBaseCheckStateRejectsConfiguredActorMismatch() public {
|
||||||
|
(string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("check-base-mismatch");
|
||||||
|
_setBaseConfig(OWNER, ALICE, "https://public.invalid");
|
||||||
|
CheckStateHarness checker = new CheckStateHarness();
|
||||||
|
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(
|
||||||
|
DemoScript.UnexpectedAddress.selector, "Recipient", manifest.actors[1].address_, ALICE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
checker.runWithPath(path, "deployed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBaseTransferRejectsInvalidRecipientBeforeBroadcast() public {
|
||||||
|
(string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("transfer-base-invalid");
|
||||||
|
_setBaseConfig(OWNER, BOB, "https://public.invalid");
|
||||||
|
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
|
||||||
|
new SeedBaseSepolia().run();
|
||||||
|
string memory pending = string.concat(fixtureDir, "/base-transfer-upgrade.json");
|
||||||
|
(, address implementation) = new UpgradeV2Harness().runWithPaths(path, pending, OWNER);
|
||||||
|
vm.writeJson(string.concat('"', vm.toString(implementation), '"'), path, ".implementation");
|
||||||
|
_setBaseConfig(OWNER, OWNER, "https://public.invalid");
|
||||||
|
TransferV2DemoHarness transfer = new TransferV2DemoHarness();
|
||||||
|
|
||||||
|
vm.expectRevert(DemoScript.InvalidActorConfiguration.selector);
|
||||||
|
transfer.runWithPath(path);
|
||||||
|
|
||||||
|
BankV2 bank = BankV2(manifest.proxy);
|
||||||
|
assertEq(bank.balanceOf(OWNER), 1_000e6);
|
||||||
|
assertEq(bank.balanceOf(BOB), 0);
|
||||||
|
}
|
||||||
|
|
||||||
function _snapshot() internal pure returns (UpgradeV2.Snapshot memory snapshot) {
|
function _snapshot() internal pure returns (UpgradeV2.Snapshot memory snapshot) {
|
||||||
snapshot.proxy = PROXY;
|
snapshot.proxy = PROXY;
|
||||||
snapshot.implementation = IMPLEMENTATION;
|
snapshot.implementation = IMPLEMENTATION;
|
||||||
@@ -593,6 +766,30 @@ contract ScriptPreflightTest is Test {
|
|||||||
vm.writeFile(path, harness.serializeManifest(manifest));
|
vm.writeFile(path, harness.serializeManifest(manifest));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _deployBaseFixtureNamed(string memory name)
|
||||||
|
internal
|
||||||
|
returns (string memory path, DemoScript.Manifest memory manifest)
|
||||||
|
{
|
||||||
|
vm.chainId(84532);
|
||||||
|
MockUSDC deployedToken = new MockUSDC(OWNER);
|
||||||
|
address deployedProxy = Upgrades.deployUUPSProxy(
|
||||||
|
"BankV1.sol:BankV1", abi.encodeCall(BankV1.initialize, (address(deployedToken), OWNER))
|
||||||
|
);
|
||||||
|
manifest.schemaVersion = 1;
|
||||||
|
manifest.network = "baseSepolia";
|
||||||
|
manifest.chainId = 84532;
|
||||||
|
manifest.deploymentBlock = 1;
|
||||||
|
manifest.rpcUrl = "https://public.invalid";
|
||||||
|
manifest.explorerBaseUrl = "https://sepolia.basescan.org";
|
||||||
|
manifest.token = address(deployedToken);
|
||||||
|
manifest.proxy = deployedProxy;
|
||||||
|
manifest.implementation = Upgrades.getImplementationAddress(deployedProxy);
|
||||||
|
manifest.owner = OWNER;
|
||||||
|
manifest.actors = _baseActors();
|
||||||
|
path = string.concat(fixtureDir, "/", name, ".json");
|
||||||
|
vm.writeFile(path, harness.serializeManifest(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
function _seedState(DemoScript.Manifest memory manifest) internal {
|
function _seedState(DemoScript.Manifest memory manifest) internal {
|
||||||
MockUSDC deployedToken = MockUSDC(manifest.token);
|
MockUSDC deployedToken = MockUSDC(manifest.token);
|
||||||
BankV1 deployedBank = BankV1(manifest.proxy);
|
BankV1 deployedBank = BankV1(manifest.proxy);
|
||||||
@@ -689,7 +886,7 @@ contract ScriptPreflightTest is Test {
|
|||||||
vm.writeFile(
|
vm.writeFile(
|
||||||
path,
|
path,
|
||||||
string.concat(
|
string.concat(
|
||||||
'{"schemaVersion":1,"network":"baseSepolia","chainId":84532,"deploymentBlock":1,"token":"',
|
'{"schemaVersion":1,"network":"baseSepolia","chainId":84532,"deploymentBlock":1,"rpcUrl":"https://public.invalid","explorerBaseUrl":"https://sepolia.basescan.org","token":"',
|
||||||
vm.toString(TOKEN),
|
vm.toString(TOKEN),
|
||||||
'","proxy":"',
|
'","proxy":"',
|
||||||
vm.toString(PROXY),
|
vm.toString(PROXY),
|
||||||
@@ -697,8 +894,10 @@ contract ScriptPreflightTest is Test {
|
|||||||
vm.toString(IMPLEMENTATION),
|
vm.toString(IMPLEMENTATION),
|
||||||
'","owner":"',
|
'","owner":"',
|
||||||
vm.toString(OWNER),
|
vm.toString(OWNER),
|
||||||
'","actors":[{"label":"owner","address":"',
|
'","actors":[{"label":"Presenter","address":"',
|
||||||
vm.toString(OWNER),
|
vm.toString(OWNER),
|
||||||
|
'"},{"label":"Recipient","address":"',
|
||||||
|
vm.toString(BOB),
|
||||||
'"}]}'
|
'"}]}'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -717,6 +916,12 @@ contract ScriptPreflightTest is Test {
|
|||||||
actors[2] = DemoScript.Actor({label: "Bob", address_: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC});
|
actors[2] = DemoScript.Actor({label: "Bob", address_: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _baseActors() internal pure returns (DemoScript.Actor[] memory actors) {
|
||||||
|
actors = new DemoScript.Actor[](2);
|
||||||
|
actors[0] = DemoScript.Actor({label: "Presenter", address_: OWNER});
|
||||||
|
actors[1] = DemoScript.Actor({label: "Recipient", address_: BOB});
|
||||||
|
}
|
||||||
|
|
||||||
function _contains(string memory haystack, string memory needle) internal pure returns (bool) {
|
function _contains(string memory haystack, string memory needle) internal pure returns (bool) {
|
||||||
bytes memory h = bytes(haystack);
|
bytes memory h = bytes(haystack);
|
||||||
bytes memory n = bytes(needle);
|
bytes memory n = bytes(needle);
|
||||||
@@ -733,4 +938,11 @@ contract ScriptPreflightTest is Test {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _setBaseConfig(address sender, address recipient, string memory publicRpcUrl) internal {
|
||||||
|
vm.setEnv("SCRIPT_SENDER", vm.toString(sender));
|
||||||
|
vm.setEnv("BASE_SEPOLIA_SENDER", vm.toString(sender));
|
||||||
|
vm.setEnv("BASE_SEPOLIA_RECIPIENT", vm.toString(recipient));
|
||||||
|
vm.setEnv("BASE_SEPOLIA_PUBLIC_RPC_URL", publicRpcUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export async function finalizeUpgrade({ root = process.cwd(), rpc }) {
|
|||||||
if (matching.length !== 1) throw new Error(`expected exactly one live upgrade transaction to proxy, found ${matching.length}`);
|
if (matching.length !== 1) throw new Error(`expected exactly one live upgrade transaction to proxy, found ${matching.length}`);
|
||||||
const receipt = await rpc("eth_getTransactionReceipt", [matching[0].hash]);
|
const receipt = await rpc("eth_getTransactionReceipt", [matching[0].hash]);
|
||||||
if (!receipt || !isSuccessfulReceipt(receipt.status)) throw new Error("upgrade receipt was not successful");
|
if (!receipt || !isSuccessfulReceipt(receipt.status)) throw new Error("upgrade receipt was not successful");
|
||||||
|
const upgradeBlock = parseRpcQuantity(receipt.blockNumber, "upgrade receipt block number");
|
||||||
|
if (upgradeBlock === 0) throw new Error("upgrade receipt block number must be nonzero");
|
||||||
if (!Array.isArray(receipt.logs) || !receipt.logs.some((log) => isUpgradeLog(log, active.proxy, pending.implementation))) {
|
if (!Array.isArray(receipt.logs) || !receipt.logs.some((log) => isUpgradeLog(log, active.proxy, pending.implementation))) {
|
||||||
throw new Error("successful upgrade receipt is missing the expected Upgraded event");
|
throw new Error("successful upgrade receipt is missing the expected Upgraded event");
|
||||||
}
|
}
|
||||||
@@ -148,7 +150,7 @@ export async function finalizeUpgrade({ root = process.cwd(), rpc }) {
|
|||||||
await atomicWriteJson(canonicalPath, updated);
|
await atomicWriteJson(canonicalPath, updated);
|
||||||
await atomicWriteJson(activePath, updated);
|
await atomicWriteJson(activePath, updated);
|
||||||
await rm(pendingPath);
|
await rm(pendingPath);
|
||||||
return { mode: "upgrade", path: canonicalPath, manifest: updated };
|
return { mode: "upgrade", path: canonicalPath, manifest: updated, upgradeBlock };
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateNoopMarker(marker, active) {
|
function validateNoopMarker(marker, active) {
|
||||||
@@ -377,6 +379,11 @@ function assertPublicUrl(value, field) {
|
|||||||
if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username || url.password) {
|
if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username || url.password) {
|
||||||
throw new Error(`manifest ${field} must be a public URL without credentials`);
|
throw new Error(`manifest ${field} must be a public URL without credentials`);
|
||||||
}
|
}
|
||||||
|
for (const key of url.searchParams.keys()) {
|
||||||
|
if (/(key|token|secret|password|credential)/i.test(key)) {
|
||||||
|
throw new Error(`manifest ${field} must not contain credential query parameters`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertActorConfiguration(manifest) {
|
function assertActorConfiguration(manifest) {
|
||||||
@@ -390,8 +397,13 @@ function assertActorConfiguration(manifest) {
|
|||||||
if (actors.length !== expected.length || actors.some((actor, index) => actor.label !== expected[index][0] || actor.address.toLowerCase() !== expected[index][1].toLowerCase())) {
|
if (actors.length !== expected.length || actors.some((actor, index) => actor.label !== expected[index][0] || actor.address.toLowerCase() !== expected[index][1].toLowerCase())) {
|
||||||
throw new Error("manifest anvil actors must match the documented local actor configuration");
|
throw new Error("manifest anvil actors must match the documented local actor configuration");
|
||||||
}
|
}
|
||||||
} else if (actors.length !== 1 || actors[0].label !== "owner") {
|
} else {
|
||||||
throw new Error("manifest baseSepolia must contain only the owner actor");
|
const configured = actors.length === 2 && actors[0].label === "Presenter" && actors[1].label === "Recipient";
|
||||||
|
const legacy = actors.length === 1 && actors[0].label === "owner"
|
||||||
|
&& !Object.hasOwn(manifest, "rpcUrl") && !Object.hasOwn(manifest, "explorerBaseUrl");
|
||||||
|
if (!configured && !legacy) {
|
||||||
|
throw new Error("manifest baseSepolia actors must be Presenter and Recipient");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (actors[0].address.toLowerCase() !== owner.toLowerCase()) throw new Error("manifest owner must be actor zero");
|
if (actors[0].address.toLowerCase() !== owner.toLowerCase()) throw new Error("manifest owner must be actor zero");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ export function validatePublicManifest(manifest) {
|
|||||||
for (const field of ["token", "proxy", "implementation", "owner"]) assertAddress(manifest[field], field);
|
for (const field of ["token", "proxy", "implementation", "owner"]) assertAddress(manifest[field], field);
|
||||||
assertActors(manifest.actors);
|
assertActors(manifest.actors);
|
||||||
for (const field of optionalFields) if (Object.hasOwn(manifest, field)) assertPublicUrl(manifest[field], field);
|
for (const field of optionalFields) if (Object.hasOwn(manifest, field)) assertPublicUrl(manifest[field], field);
|
||||||
|
if (manifest.network === "baseSepolia") {
|
||||||
|
if (Object.hasOwn(manifest, "rpcUrl") && new URL(manifest.rpcUrl).protocol !== "https:") {
|
||||||
|
throw new Error("manifest Base Sepolia rpcUrl must use HTTPS");
|
||||||
|
}
|
||||||
|
if (Object.hasOwn(manifest, "explorerBaseUrl") && manifest.explorerBaseUrl !== "https://sepolia.basescan.org") {
|
||||||
|
throw new Error("manifest Base Sepolia explorerBaseUrl must use BaseScan");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
||||||
|
|||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '%s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case ${1-} in
|
||||||
|
deploy|upgrade|transfer) ;;
|
||||||
|
*) fail 'usage: require-base-config.sh <deploy|upgrade|transfer>' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for variable in \
|
||||||
|
BASE_SEPOLIA_RPC_URL \
|
||||||
|
BASE_SEPOLIA_PUBLIC_RPC_URL \
|
||||||
|
BASE_SEPOLIA_ACCOUNT \
|
||||||
|
BASE_SEPOLIA_SENDER \
|
||||||
|
BASE_SEPOLIA_RECIPIENT; do
|
||||||
|
[[ -n ${!variable-} ]] || fail "$variable is required"
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ $BASE_SEPOLIA_ACCOUNT =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]] || \
|
||||||
|
fail 'BASE_SEPOLIA_ACCOUNT must be a conservative Foundry account identifier'
|
||||||
|
|
||||||
|
address_pattern='^0x[[:xdigit:]]{40}$'
|
||||||
|
[[ $BASE_SEPOLIA_SENDER =~ $address_pattern ]] || fail 'BASE_SEPOLIA_SENDER must be an address'
|
||||||
|
[[ $BASE_SEPOLIA_RECIPIENT =~ $address_pattern ]] || fail 'BASE_SEPOLIA_RECIPIENT must be an address'
|
||||||
|
[[ ${BASE_SEPOLIA_SENDER,,} != 0x0000000000000000000000000000000000000000 ]] || \
|
||||||
|
fail 'BASE_SEPOLIA_SENDER must be nonzero'
|
||||||
|
[[ ${BASE_SEPOLIA_RECIPIENT,,} != 0x0000000000000000000000000000000000000000 ]] || \
|
||||||
|
fail 'BASE_SEPOLIA_RECIPIENT must be nonzero'
|
||||||
|
[[ ${BASE_SEPOLIA_SENDER,,} != "${BASE_SEPOLIA_RECIPIENT,,}" ]] || \
|
||||||
|
fail 'BASE_SEPOLIA_SENDER and BASE_SEPOLIA_RECIPIENT must differ'
|
||||||
|
|
||||||
|
[[ $BASE_SEPOLIA_RPC_URL =~ ^https://[^[:space:]]+$ ]] || \
|
||||||
|
fail 'BASE_SEPOLIA_RPC_URL must use HTTPS'
|
||||||
|
[[ $BASE_SEPOLIA_PUBLIC_RPC_URL =~ ^https://[^[:space:]]+$ ]] || \
|
||||||
|
fail 'BASE_SEPOLIA_PUBLIC_RPC_URL must use HTTPS'
|
||||||
|
|
||||||
|
public_remainder=${BASE_SEPOLIA_PUBLIC_RPC_URL#https://}
|
||||||
|
public_authority=${public_remainder%%[/?#]*}
|
||||||
|
[[ -n $public_authority && $public_authority != *@* ]] || \
|
||||||
|
fail 'BASE_SEPOLIA_PUBLIC_RPC_URL must not contain user info'
|
||||||
|
|
||||||
|
if [[ $BASE_SEPOLIA_PUBLIC_RPC_URL == *\?* ]]; then
|
||||||
|
public_query=${BASE_SEPOLIA_PUBLIC_RPC_URL#*\?}
|
||||||
|
public_query=${public_query%%#*}
|
||||||
|
IFS='&' read -r -a parameters <<<"$public_query"
|
||||||
|
for parameter in "${parameters[@]}"; do
|
||||||
|
key=${parameter%%=*}
|
||||||
|
if [[ ${key,,} =~ (key|token|secret|password|credential) ]]; then
|
||||||
|
fail 'BASE_SEPOLIA_PUBLIC_RPC_URL must not contain credential query parameters'
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
import { access, readFile, rename } from "node:fs/promises";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
@@ -14,9 +14,33 @@ export async function selectManifest({ root = process.cwd(), network }) {
|
|||||||
return { source, active };
|
return { source, active };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function archiveManifest({ root = process.cwd(), network, now = new Date() }) {
|
||||||
|
if (network !== "baseSepolia") throw new Error("only baseSepolia may be archived");
|
||||||
|
const spec = networkSpec(network);
|
||||||
|
const source = join(root, "deployments", spec.canonical);
|
||||||
|
await readManifest(source);
|
||||||
|
if (!(now instanceof Date) || Number.isNaN(now.valueOf())) throw new Error("archive timestamp is invalid");
|
||||||
|
const timestamp = now.toISOString().replace(/[-:.]/g, "");
|
||||||
|
if (!/^\d{8}T\d{9}Z$/.test(timestamp)) throw new Error("archive timestamp is invalid");
|
||||||
|
const target = join(root, "deployments", `base-sepolia.${timestamp}.json`);
|
||||||
|
try {
|
||||||
|
await access(target);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "ENOENT") {
|
||||||
|
await rename(source, target);
|
||||||
|
return { source, path: target };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error("refusing to overwrite an existing Base Sepolia archive");
|
||||||
|
}
|
||||||
|
|
||||||
export async function runSelectCli(argv, { root = process.cwd(), log = console.log } = {}) {
|
export async function runSelectCli(argv, { root = process.cwd(), log = console.log } = {}) {
|
||||||
log(EDUCATIONAL_WARNING);
|
log(EDUCATIONAL_WARNING);
|
||||||
if (argv.length !== 1) throw new Error("usage: select-manifest.mjs <anvil|baseSepolia>");
|
if (argv.length === 2 && argv[0] === "archive" && argv[1] === "baseSepolia") {
|
||||||
|
return archiveManifest({ root, network: argv[1] });
|
||||||
|
}
|
||||||
|
if (argv.length !== 1) throw new Error("usage: select-manifest.mjs <anvil|baseSepolia> | archive baseSepolia");
|
||||||
return selectManifest({ root, network: argv[0] });
|
return selectManifest({ root, network: argv[0] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+188
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repository_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||||
|
guard="$repository_root/tools/require-base-config.sh"
|
||||||
|
terminal_url=https://terminal.invalid/rpc
|
||||||
|
public_url=https://public.invalid/rpc
|
||||||
|
account=demo-account
|
||||||
|
sender=0x1111111111111111111111111111111111111111
|
||||||
|
recipient=0x2222222222222222222222222222222222222222
|
||||||
|
|
||||||
|
run_guard() {
|
||||||
|
local mode=${1-deploy}
|
||||||
|
shift || true
|
||||||
|
env -i PATH="$PATH" \
|
||||||
|
BASE_SEPOLIA_RPC_URL="$terminal_url" \
|
||||||
|
BASE_SEPOLIA_PUBLIC_RPC_URL="$public_url" \
|
||||||
|
BASE_SEPOLIA_ACCOUNT="$account" \
|
||||||
|
BASE_SEPOLIA_SENDER="$sender" \
|
||||||
|
BASE_SEPOLIA_RECIPIENT="$recipient" \
|
||||||
|
"$@" bash "$guard" "$mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_no_config_values() {
|
||||||
|
local output=$1
|
||||||
|
shift
|
||||||
|
local value
|
||||||
|
for value in "$terminal_url" "$public_url" "$account" "$sender" "$recipient" "$@"; do
|
||||||
|
[[ -n $value ]] || continue
|
||||||
|
if [[ $output == *"$value"* ]]; then
|
||||||
|
echo "configuration guard leaked a configured value" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_rejected() {
|
||||||
|
local assignment=$1
|
||||||
|
local output
|
||||||
|
if output=$(run_guard deploy "$assignment" 2>&1); then
|
||||||
|
echo "configuration guard accepted invalid fixture: ${assignment%%=*}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
assert_no_config_values "$output" "${assignment#*=}"
|
||||||
|
}
|
||||||
|
|
||||||
|
for mode in deploy upgrade transfer; do
|
||||||
|
output=$(run_guard "$mode" 2>&1)
|
||||||
|
[[ -z $output ]] || { echo "configuration guard produced output for valid fixtures" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
for assignment in \
|
||||||
|
BASE_SEPOLIA_RPC_URL= \
|
||||||
|
BASE_SEPOLIA_PUBLIC_RPC_URL= \
|
||||||
|
BASE_SEPOLIA_ACCOUNT= \
|
||||||
|
BASE_SEPOLIA_SENDER= \
|
||||||
|
BASE_SEPOLIA_RECIPIENT= \
|
||||||
|
BASE_SEPOLIA_ACCOUNT='bad account' \
|
||||||
|
BASE_SEPOLIA_SENDER=0x1234 \
|
||||||
|
BASE_SEPOLIA_RECIPIENT=0x0000000000000000000000000000000000000000 \
|
||||||
|
BASE_SEPOLIA_RECIPIENT="$sender" \
|
||||||
|
BASE_SEPOLIA_RPC_URL=http://terminal.invalid \
|
||||||
|
BASE_SEPOLIA_PUBLIC_RPC_URL=http://public.invalid \
|
||||||
|
BASE_SEPOLIA_PUBLIC_RPC_URL=https://user@public.invalid \
|
||||||
|
BASE_SEPOLIA_PUBLIC_RPC_URL='https://public.invalid/?api_key=fixture' \
|
||||||
|
BASE_SEPOLIA_PUBLIC_RPC_URL='https://public.invalid/?token=fixture'; do
|
||||||
|
expect_rejected "$assignment"
|
||||||
|
done
|
||||||
|
|
||||||
|
private_key_name=PRIVATE_$(printf KEY)
|
||||||
|
mnemonic_name=MNEM$(printf ONIC)
|
||||||
|
fake_key=0x$(printf 'a%.0s' {1..64})
|
||||||
|
output=$(
|
||||||
|
env -i PATH="$PATH" "$private_key_name=$fake_key" "$mnemonic_name=fixture words only" \
|
||||||
|
bash "$guard" deploy 2>&1 || true
|
||||||
|
)
|
||||||
|
if [[ $output != *BASE_SEPOLIA_RPC_URL* ]]; then
|
||||||
|
echo "configuration guard unexpectedly accepted a raw-key or mnemonic fallback" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
assert_no_config_values "$output"
|
||||||
|
|
||||||
|
dry_run=$(
|
||||||
|
make -n -C "$repository_root" 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
|
||||||
|
)
|
||||||
|
[[ $dry_run == *'--account "demo"'* ]] || { echo "Base dry run omitted --account" >&2; exit 1; }
|
||||||
|
[[ $dry_run == *'--sender "0x1111111111111111111111111111111111111111"'* ]] || \
|
||||||
|
{ echo "Base dry run omitted --sender" >&2; exit 1; }
|
||||||
|
[[ $dry_run == *'--slow'* ]] || { echo "Base dry run omitted --slow" >&2; exit 1; }
|
||||||
|
if [[ ${dry_run,,} == *private-key* || ${dry_run,,} == *mnemonic* ]]; then
|
||||||
|
echo "Base dry run exposed a raw signing option" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
node --input-type=module - "$repository_root" <<'NODE'
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
const repositoryRoot = process.argv[2];
|
||||||
|
const { archiveManifest, selectManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/select-manifest.mjs")));
|
||||||
|
const { publishManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/publish-web-manifest.mjs")));
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "uups-base-config-"));
|
||||||
|
const deployments = join(root, "deployments");
|
||||||
|
const webManifest = join(root, "web/public/deployment.json");
|
||||||
|
const owner = "0x1111111111111111111111111111111111111111";
|
||||||
|
const recipient = "0x2222222222222222222222222222222222222222";
|
||||||
|
const base = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
network: "baseSepolia",
|
||||||
|
chainId: 84532,
|
||||||
|
deploymentBlock: 99,
|
||||||
|
rpcUrl: "https://public.invalid/rpc",
|
||||||
|
explorerBaseUrl: "https://sepolia.basescan.org",
|
||||||
|
token: "0x3333333333333333333333333333333333333333",
|
||||||
|
proxy: "0x4444444444444444444444444444444444444444",
|
||||||
|
implementation: "0x5555555555555555555555555555555555555555",
|
||||||
|
owner,
|
||||||
|
actors: [
|
||||||
|
{ label: "Presenter", address: owner },
|
||||||
|
{ label: "Recipient", address: recipient },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const anvil = {
|
||||||
|
...base,
|
||||||
|
network: "anvil",
|
||||||
|
chainId: 31337,
|
||||||
|
rpcUrl: "http://127.0.0.1:8545",
|
||||||
|
explorerBaseUrl: undefined,
|
||||||
|
actors: [
|
||||||
|
{ label: "owner", address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" },
|
||||||
|
{ label: "Alice", address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" },
|
||||||
|
{ label: "Bob", address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" },
|
||||||
|
],
|
||||||
|
owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||||
|
};
|
||||||
|
delete anvil.explorerBaseUrl;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mkdir(deployments, { recursive: true });
|
||||||
|
await writeFile(join(deployments, "base-sepolia.json"), `${JSON.stringify(base)}\n`);
|
||||||
|
await writeFile(join(deployments, "anvil.json"), `${JSON.stringify(anvil)}\n`);
|
||||||
|
await selectManifest({ root, network: "baseSepolia" });
|
||||||
|
const activeBefore = await readFile(join(deployments, "active.json"));
|
||||||
|
const anvilBefore = await readFile(join(deployments, "anvil.json"));
|
||||||
|
const published = await publishManifest({
|
||||||
|
activePath: join(deployments, "active.json"),
|
||||||
|
outputPath: webManifest,
|
||||||
|
});
|
||||||
|
assert.equal(published.rpcUrl, "https://public.invalid/rpc");
|
||||||
|
assert.equal(published.explorerBaseUrl, "https://sepolia.basescan.org");
|
||||||
|
assert.equal((await readFile(webManifest, "utf8")).includes("terminal.invalid"), false);
|
||||||
|
for (const invalid of [
|
||||||
|
{ ...base, rpcUrl: "http://public.invalid/rpc" },
|
||||||
|
{ ...base, explorerBaseUrl: "https://example.invalid" },
|
||||||
|
]) {
|
||||||
|
await writeFile(join(deployments, "active.json"), `${JSON.stringify(invalid)}\n`);
|
||||||
|
await assert.rejects(
|
||||||
|
() => publishManifest({ activePath: join(deployments, "active.json"), outputPath: webManifest }),
|
||||||
|
/HTTPS|BaseScan/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await writeFile(join(deployments, "active.json"), activeBefore);
|
||||||
|
|
||||||
|
const archived = await archiveManifest({
|
||||||
|
root,
|
||||||
|
network: "baseSepolia",
|
||||||
|
now: new Date("2026-08-21T12:34:56.789Z"),
|
||||||
|
});
|
||||||
|
assert.equal(archived.path, join(deployments, "base-sepolia.20260821T123456789Z.json"));
|
||||||
|
await access(archived.path);
|
||||||
|
await assert.rejects(() => access(join(deployments, "base-sepolia.json")));
|
||||||
|
assert.deepEqual(await readFile(join(deployments, "active.json")), activeBefore);
|
||||||
|
assert.deepEqual(await readFile(join(deployments, "anvil.json")), anvilBefore);
|
||||||
|
await assert.rejects(() => archiveManifest({ root, network: "anvil" }), /baseSepolia/);
|
||||||
|
} finally {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
NODE
|
||||||
|
|
||||||
|
echo "Base configuration and manifest filesystem tests passed"
|
||||||
Reference in New Issue
Block a user