Files

949 lines
41 KiB
Solidity

// 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 {DemoScript} from "../script/lib/DemoScript.sol";
import {DeployV1} from "../script/DeployV1.s.sol";
import {SeedV1Demo} from "../script/SeedV1Demo.s.sol";
import {SeedBaseSepolia} from "../script/SeedBaseSepolia.s.sol";
import {CheckState} from "../script/CheckState.s.sol";
import {UpgradeV2} from "../script/UpgradeV2.s.sol";
import {TransferV2Demo} from "../script/TransferV2Demo.s.sol";
import {BankV1} from "../src/BankV1.sol";
import {BankV2} from "../src/BankV2.sol";
import {MockUSDC} from "../src/MockUSDC.sol";
contract ScriptPreflightHarness is DemoScript {
function requireSupportedChain(uint256 chainId) external pure {
_requireSupportedChain(chainId);
}
function deriveLocalActor(uint256 chainId, uint32 index) external pure returns (address actor) {
(, actor) = _deriveLocalActor(chainId, index);
}
function readManifest(string calldata path, bool active) external view returns (Manifest memory) {
return _readManifest(path, active);
}
function requireCode(string calldata label, address target) external view {
_requireCode(label, target);
}
function serializeManifest(Manifest calldata manifest) external returns (string memory) {
return _serializeManifest(manifest);
}
function educationalWarning() external pure returns (string memory) {
return _educationalWarning();
}
}
contract UpgradeV2Harness is UpgradeV2 {
function assertSnapshotUnchanged(Snapshot calldata before_, Snapshot calldata after_) external pure {
_assertSnapshotUnchanged(before_, after_);
}
function runWithPaths(string calldata activePath, string calldata pendingPath, address sender)
external
returns (bool upgraded, address implementation)
{
_requireSupportedChain(block.chainid);
return _run(activePath, pendingPath, sender);
}
}
contract TransferV2DemoHarness is TransferV2Demo {
function runWithPath(string calldata manifestPath) external {
_run(manifestPath);
}
}
contract CheckStateHarness is CheckState {
function runWithPath(string calldata manifestPath, string calldata stage) external view {
_run(manifestPath, stage);
}
}
contract ScriptPreflightTest is Test {
ScriptPreflightHarness internal harness;
string internal fixtureDir;
address internal constant OWNER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
address internal constant ALICE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
address internal constant BOB = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;
address internal constant TOKEN = 0x1000000000000000000000000000000000000001;
address internal constant PROXY = 0x2000000000000000000000000000000000000002;
address internal constant IMPLEMENTATION = 0x3000000000000000000000000000000000000003;
function setUp() public {
harness = new ScriptPreflightHarness();
fixtureDir = string.concat(vm.projectRoot(), "/deployments/test-script-preflight");
vm.createDir(fixtureDir, true);
}
function testSupportedChainsAreAccepted() public view {
harness.requireSupportedChain(31337);
harness.requireSupportedChain(84532);
}
function testUnsupportedChainsAreRejectedBeforeBroadcast() public {
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) {
vm.expectRevert(abi.encodeWithSelector(DemoScript.UnsupportedChain.selector, rejected[i]));
harness.requireSupportedChain(rejected[i]);
}
}
function testLocalActorsDeriveOnlyOnAnvil() public {
assertEq(harness.deriveLocalActor(31337, 0), OWNER);
assertEq(harness.deriveLocalActor(31337, 1), ALICE);
vm.expectRevert(abi.encodeWithSelector(DemoScript.UnsupportedChain.selector, uint256(84532)));
harness.deriveLocalActor(84532, 0);
}
function testMissingManifestIsRejected() public {
vm.expectRevert();
harness.readManifest(string.concat(fixtureDir, "/missing.json"), false);
}
function testInvalidManifestIsRejected() public {
string memory path = string.concat(fixtureDir, "/invalid.json");
vm.writeFile(path, "not-json");
vm.expectRevert();
harness.readManifest(path, false);
}
function testWrongManifestChainIsRejected() public {
string memory path = _writeManifest(84532, 1, TOKEN, PROXY, IMPLEMENTATION);
vm.chainId(31337);
vm.expectRevert(
abi.encodeWithSelector(DemoScript.ManifestChainMismatch.selector, uint256(31337), uint256(84532))
);
harness.readManifest(path, true);
}
function testZeroManifestAddressIsRejected() public {
string memory path = _writeManifest(31337, 1, address(0), PROXY, IMPLEMENTATION);
vm.chainId(31337);
vm.expectRevert(abi.encodeWithSelector(DemoScript.MissingCode.selector, "token", address(0)));
harness.readManifest(path, true);
}
function testAddressWithoutCodeIsRejected() public {
vm.expectRevert(abi.encodeWithSelector(DemoScript.MissingCode.selector, "token", TOKEN));
harness.requireCode("token", TOKEN);
}
function testPendingManifestMayUseDeploymentBlockZero() public {
string memory path = _writeManifest(31337, 0, TOKEN, PROXY, IMPLEMENTATION);
vm.chainId(31337);
vm.etch(TOKEN, hex"00");
vm.etch(PROXY, hex"00");
vm.etch(IMPLEMENTATION, hex"00");
DemoScript.Manifest memory manifest = harness.readManifest(path, false);
assertEq(manifest.deploymentBlock, 0);
}
function testActiveManifestRejectsDeploymentBlockZero() public {
string memory path = _writeManifest(31337, 0, TOKEN, PROXY, IMPLEMENTATION);
vm.chainId(31337);
vm.etch(TOKEN, hex"00");
vm.etch(PROXY, hex"00");
vm.etch(IMPLEMENTATION, hex"00");
vm.expectRevert(DemoScript.InvalidDeploymentBlock.selector);
harness.readManifest(path, true);
}
function testAnvilManifestRejectsUnexpectedActorObjectCount() public {
string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION);
_etchManifestContracts();
vm.writeJson(
string.concat(
'[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"}]'
),
path,
".actors"
);
vm.expectRevert();
harness.readManifest(path, true);
}
function testAnvilManifestRejectsUnexpectedActorLabels() public {
string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION);
_etchManifestContracts();
vm.writeJson(
string.concat(
'[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Mallory","address":"',
vm.toString(ALICE),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]'
),
path,
".actors"
);
vm.expectRevert(DemoScript.InvalidActorConfiguration.selector);
harness.readManifest(path, true);
}
function testAnvilManifestRequiresOwnerAtActorZero() public {
string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION);
_etchManifestContracts();
vm.writeJson(
string.concat(
'[{"label":"owner","address":"',
vm.toString(ALICE),
'"},{"label":"Alice","address":"',
vm.toString(OWNER),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]'
),
path,
".actors"
);
vm.expectRevert(DemoScript.InvalidActorConfiguration.selector);
harness.readManifest(path, true);
}
function testBaseManifestRequiresExactPresenterRecipientActors() public {
string memory path = _writePublicBaseManifest();
vm.chainId(84532);
_etchManifestContracts();
vm.writeJson(
string.concat(
'[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"}]'
),
path,
".actors"
);
vm.expectRevert(DemoScript.InvalidActorConfiguration.selector);
harness.readManifest(path, true);
}
function testEducationalWarningIsExact() public view {
assertEq(harness.educationalWarning(), unicode"Educational demo — mock token — never use real funds.");
}
function testNestedActorsRoundTripThroughManifestSerialization() public {
string memory path = _writePublicAnvilManifest();
_etchManifestContracts();
DemoScript.Manifest memory manifest = harness.readManifest(path, true);
assertEq(manifest.actors[0].label, "owner");
assertEq(manifest.actors[1].address_, ALICE);
string memory serialized = harness.serializeManifest(manifest);
assertFalse(_contains(serialized, "actorLabels"));
assertFalse(_contains(serialized, "explorerUrl"));
vm.writeFile(path, serialized);
DemoScript.Manifest memory roundTrip = harness.readManifest(path, true);
assertEq(roundTrip.actors[2].label, "Bob");
assertEq(roundTrip.actors[2].address_, 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC);
}
function testLegacyParallelActorManifestIsRejected() public {
string memory path = _writeLegacyParallelActorManifest();
_etchManifestContracts();
vm.expectRevert(abi.encodeWithSelector(DemoScript.InvalidManifestSchema.selector, uint256(0)));
harness.readManifest(path, true);
}
function testActorObjectWithExtraKeyIsRejected() public {
string memory path = _writeAnvilManifestWithExtraActorKey("note", "public");
_etchManifestContracts();
vm.expectRevert(abi.encodeWithSelector(DemoScript.InvalidManifestSchema.selector, uint256(0)));
harness.readManifest(path, true);
}
function testActorObjectWithSecretBearingKeyIsRejected() public {
string memory path = _writeAnvilManifestWithExtraActorKey("privateKey", "not-a-key");
_etchManifestContracts();
vm.expectRevert(abi.encodeWithSelector(DemoScript.InvalidManifestSchema.selector, uint256(0)));
harness.readManifest(path, true);
}
function testBaseSepoliaManifestUsesPublicUrlsAndExactActors() public {
string memory path = _writePublicBaseManifest();
vm.chainId(84532);
_etchManifestContracts();
DemoScript.Manifest memory manifest = harness.readManifest(path, true);
assertEq(manifest.network, "baseSepolia");
assertEq(manifest.rpcUrl, "https://public.invalid");
assertEq(manifest.explorerBaseUrl, "https://sepolia.basescan.org");
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 testSerializedManifestContainsPublicAddressesAndNoSecrets() public {
DemoScript.Manifest memory manifest = DemoScript.Manifest({
schemaVersion: 1,
network: "anvil",
chainId: 31337,
deploymentBlock: 0,
rpcUrl: "http://127.0.0.1:8545",
explorerBaseUrl: "",
token: TOKEN,
proxy: PROXY,
implementation: IMPLEMENTATION,
owner: OWNER,
actors: _actors()
});
string memory json = harness.serializeManifest(manifest);
assertTrue(_contains(json, vm.toString(TOKEN)));
assertTrue(_contains(json, vm.toString(PROXY)));
assertTrue(_contains(json, vm.toString(IMPLEMENTATION)));
assertTrue(_contains(json, vm.toString(OWNER)));
assertFalse(_contains(json, "test test test test test test test test test test test junk"));
assertFalse(_contains(json, "PRIVATE_KEY"));
assertFalse(_contains(json, "MNEMONIC"));
assertFalse(_contains(json, "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"));
}
function testDeployV1CreatesInitializedProxyAndPublicPendingManifest() public {
string memory path = string.concat(fixtureDir, "/deployed.json");
vm.chainId(31337);
vm.setEnv("SCRIPT_SENDER", vm.toString(OWNER));
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
DeployV1 deployer = new DeployV1();
(address token, address proxy, address implementation) = deployer.run();
DemoScript.Manifest memory manifest = harness.readManifest(path, false);
assertEq(manifest.token, token);
assertEq(manifest.proxy, proxy);
assertEq(manifest.implementation, implementation);
assertEq(BankV1(proxy).owner(), OWNER);
assertEq(address(BankV1(proxy).asset()), token);
assertEq(BankV1(proxy).contractVersion(), 1);
assertEq(manifest.actors[0].label, "owner");
assertEq(manifest.actors[1].label, "Alice");
assertEq(manifest.actors[2].label, "Bob");
assertEq(manifest.actors[0].address_, OWNER);
assertEq(manifest.actors[1].address_, ALICE);
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 {
(string memory path, DemoScript.Manifest memory manifest) = _deployFixtureNamed("seed-v1");
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
new SeedV1Demo().run();
BankV1 bank = BankV1(manifest.proxy);
assertEq(bank.balanceOf(manifest.actors[1].address_), 900e6);
assertEq(bank.balanceOf(manifest.actors[2].address_), 500e6);
assertEq(bank.totalLiabilities(), 1_400e6);
assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6);
assertEq(MockUSDC(manifest.token).balanceOf(manifest.actors[1].address_), 1_100e6);
assertEq(MockUSDC(manifest.token).balanceOf(manifest.actors[2].address_), 500e6);
vm.setEnv("DEMO_EXPECTED_STAGE", "v1");
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 {
(string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("check-mismatch");
vm.writeJson(string.concat('"', vm.toString(manifest.token), '"'), path, ".implementation");
CheckStateHarness checker = new CheckStateHarness();
vm.expectRevert(
abi.encodeWithSelector(
DemoScript.UnexpectedAddress.selector, "implementation", manifest.token, manifest.implementation
)
);
checker.runWithPath(path, "deployed");
}
function testUpgradeV2RequiresManifestIdentityCodeOwnerSenderAndVersionOne() public {
(string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("upgrade-preflight");
string memory pending = string.concat(fixtureDir, "/upgrade-pending.json");
UpgradeV2Harness upgrader = new UpgradeV2Harness();
vm.expectRevert(
abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "SCRIPT_SENDER", manifest.owner, ALICE)
);
upgrader.runWithPaths(path, pending, ALICE);
vm.writeJson(string.concat('"', vm.toString(manifest.token), '"'), path, ".implementation");
vm.expectRevert(
abi.encodeWithSelector(
DemoScript.UnexpectedAddress.selector, "implementation", manifest.token, manifest.implementation
)
);
upgrader.runWithPaths(path, pending, OWNER);
}
function testUpgradeV2RejectsUnsupportedChainMissingProxyCodeWrongOwnerAndUnexpectedVersion() public {
UpgradeV2Harness upgrader = new UpgradeV2Harness();
vm.chainId(1);
vm.expectRevert(abi.encodeWithSelector(DemoScript.UnsupportedChain.selector, uint256(1)));
upgrader.runWithPaths("unused", "unused", OWNER);
vm.chainId(31337);
(string memory missingPath,) = _deployUpgradeFixtureNamed("upgrade-missing-proxy");
vm.writeJson('"0x4000000000000000000000000000000000000004"', missingPath, ".proxy");
vm.expectRevert(
abi.encodeWithSelector(DemoScript.MissingCode.selector, "proxy", 0x4000000000000000000000000000000000000004)
);
upgrader.runWithPaths(missingPath, string.concat(fixtureDir, "/missing-pending.json"), OWNER);
(string memory ownerPath, DemoScript.Manifest memory ownerManifest) =
_deployUpgradeFixtureNamed("upgrade-owner");
vm.prank(OWNER);
BankV1(ownerManifest.proxy).transferOwnership(ALICE);
vm.expectRevert(abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "owner", OWNER, ALICE));
upgrader.runWithPaths(ownerPath, string.concat(fixtureDir, "/owner-pending.json"), ALICE);
(string memory versionPath, DemoScript.Manifest memory versionManifest) =
_deployUpgradeFixtureNamed("upgrade-version");
vm.mockCall(
versionManifest.proxy, abi.encodeWithSelector(BankV1.contractVersion.selector), abi.encode(uint256(3))
);
vm.expectRevert(abi.encodeWithSelector(UpgradeV2.UnexpectedVersion.selector, uint256(3)));
upgrader.runWithPaths(versionPath, string.concat(fixtureDir, "/version-pending.json"), OWNER);
}
function testUpgradeV2PreservesCompleteSnapshotAndWritesOnlyStagingRecord() public {
(string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("upgrade-preserve");
_seedState(manifest);
string memory beforeManifest = vm.readFile(path);
string memory pending = string.concat(fixtureDir, "/upgrade-pending.json");
(bool upgraded, address implementation) = new UpgradeV2Harness().runWithPaths(path, pending, OWNER);
assertTrue(upgraded);
assertNotEq(implementation, manifest.implementation);
assertEq(vm.readFile(path), beforeManifest);
BankV2 bankV2 = BankV2(manifest.proxy);
assertEq(bankV2.contractVersion(), 2);
assertEq(bankV2.owner(), manifest.owner);
assertEq(address(bankV2.asset()), manifest.token);
assertFalse(bankV2.paused());
assertEq(bankV2.balanceOf(ALICE), 900e6);
assertEq(bankV2.balanceOf(BOB), 500e6);
assertEq(bankV2.totalLiabilities(), 1_400e6);
assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6);
string memory marker = vm.readFile(pending);
assertEq(vm.parseJsonString(marker, ".mode"), "upgrade");
assertEq(vm.parseJsonAddress(marker, ".implementation"), implementation);
assertEq(vm.parseJsonAddress(marker, ".snapshot.proxy"), manifest.proxy);
assertEq(vm.parseJsonUint(marker, ".snapshot.balances[1].balance"), 900e6);
assertEq(vm.parseJsonUint(marker, ".snapshot.balances[2].balance"), 500e6);
}
function testUpgradeV2AlreadyActiveWritesVerifiableNoopWithoutChangingImplementation() public {
(string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("upgrade-noop");
string memory pending = string.concat(fixtureDir, "/upgrade-pending.json");
UpgradeV2Harness upgrader = new UpgradeV2Harness();
(, address implementation) = upgrader.runWithPaths(path, pending, OWNER);
vm.writeJson(string.concat('"', vm.toString(implementation), '"'), path, ".implementation");
uint256 nonceBefore = vm.getNonce(OWNER);
(bool upgraded, address observedImplementation) = upgrader.runWithPaths(path, pending, OWNER);
assertFalse(upgraded);
assertEq(observedImplementation, implementation);
assertEq(vm.getNonce(OWNER), nonceBefore);
string memory marker = vm.readFile(pending);
assertEq(vm.parseJsonString(marker, ".mode"), "noop");
assertEq(vm.parseJsonUint(marker, ".chainId"), 31337);
assertEq(vm.parseJsonUint(marker, ".observedBlock"), block.number);
assertEq(vm.parseJsonUint(marker, ".ownerNonce"), nonceBefore);
assertEq(vm.parseJsonAddress(marker, ".proxy"), manifest.proxy);
assertEq(vm.parseJsonAddress(marker, ".implementation"), implementation);
}
function testSnapshotComparisonRejectsApplicationMutationButAllowsImplementationAndVersionChange() public {
UpgradeV2Harness upgradeHarness = new UpgradeV2Harness();
UpgradeV2.Snapshot memory before_ = _snapshot();
UpgradeV2.Snapshot memory after_ = _snapshot();
after_.implementation = address(0x9999);
after_.version = 2;
upgradeHarness.assertSnapshotUnchanged(before_, after_);
for (uint256 mutation; mutation < 10; ++mutation) {
before_ = _snapshot();
after_ = _snapshot();
if (mutation == 0) after_.proxy = address(0x9999);
else if (mutation == 1) after_.owner = address(0x9999);
else if (mutation == 2) after_.asset = address(0x9999);
else if (mutation == 3) after_.paused = true;
else if (mutation == 4) after_.balances = new uint256[](2);
else if (mutation == 5) after_.balances[1] += 1;
else if (mutation == 6) after_.liabilities += 1;
else if (mutation == 7) after_.reserves += 1;
else if (mutation == 8) after_.surplus += 1;
else after_.deploymentBlock += 1;
vm.expectRevert();
upgradeHarness.assertSnapshotUnchanged(before_, after_);
}
}
function testTransferV2DemoExecutesExactActThreeAndCheckStateAcceptsBothV2Stages() public {
(string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("transfer-v2");
_seedState(manifest);
string memory pending = string.concat(fixtureDir, "/upgrade-pending.json");
(, address implementation) = new UpgradeV2Harness().runWithPaths(path, pending, OWNER);
vm.writeJson(string.concat('"', vm.toString(implementation), '"'), path, ".implementation");
new CheckStateHarness().runWithPath(path, "upgraded");
new TransferV2DemoHarness().runWithPath(path);
new CheckStateHarness().runWithPath(path, "v2");
BankV2 bankV2 = BankV2(manifest.proxy);
assertEq(bankV2.balanceOf(ALICE), 650e6);
assertEq(bankV2.balanceOf(BOB), 750e6);
assertEq(bankV2.totalLiabilities(), 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) {
snapshot.proxy = PROXY;
snapshot.implementation = IMPLEMENTATION;
snapshot.owner = OWNER;
snapshot.asset = TOKEN;
snapshot.paused = false;
snapshot.balances = new uint256[](3);
snapshot.balances[0] = 10;
snapshot.balances[1] = 20;
snapshot.balances[2] = 30;
snapshot.liabilities = 60;
snapshot.reserves = 70;
snapshot.surplus = 10;
snapshot.deploymentBlock = 1;
snapshot.version = 1;
}
function _writeManifest(
uint256 chainId,
uint256 deploymentBlock,
address token,
address proxy,
address implementation
) internal returns (string memory path) {
path = string.concat(fixtureDir, "/manifest.json");
string memory json = string.concat(
'{"schemaVersion":1,"network":"anvil","chainId":',
vm.toString(chainId),
',"deploymentBlock":',
vm.toString(deploymentBlock),
',"rpcUrl":"http://127.0.0.1:8545","token":"',
vm.toString(token),
'","proxy":"',
vm.toString(proxy),
'","implementation":"',
vm.toString(implementation),
'","owner":"',
vm.toString(OWNER),
'","actors":[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}'
);
vm.writeFile(path, json);
}
function _deployFixture() internal returns (string memory path, DemoScript.Manifest memory manifest) {
return _deployFixtureNamed("deployed");
}
function _deployFixtureNamed(string memory name)
internal
returns (string memory path, DemoScript.Manifest memory manifest)
{
path = string.concat(fixtureDir, "/", name, ".json");
vm.chainId(31337);
vm.setEnv("SCRIPT_SENDER", vm.toString(OWNER));
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
new DeployV1().run();
vm.writeJson("1", path, ".deploymentBlock");
manifest = harness.readManifest(path, true);
}
function _deployUpgradeFixtureNamed(string memory name)
internal
returns (string memory path, DemoScript.Manifest memory manifest)
{
vm.chainId(31337);
MockUSDC deployedToken = new MockUSDC(OWNER);
address deployedProxy = Upgrades.deployUUPSProxy(
"BankV1.sol:BankV1", abi.encodeCall(BankV1.initialize, (address(deployedToken), OWNER))
);
manifest.schemaVersion = 1;
manifest.network = "anvil";
manifest.chainId = 31337;
manifest.deploymentBlock = 1;
manifest.rpcUrl = "http://127.0.0.1:8545";
manifest.token = address(deployedToken);
manifest.proxy = deployedProxy;
manifest.implementation = Upgrades.getImplementationAddress(deployedProxy);
manifest.owner = OWNER;
manifest.actors = _actors();
path = string.concat(fixtureDir, "/", name, ".json");
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 {
MockUSDC deployedToken = MockUSDC(manifest.token);
BankV1 deployedBank = BankV1(manifest.proxy);
vm.startPrank(OWNER);
deployedToken.mint(ALICE, 2_000e6);
deployedToken.mint(BOB, 1_000e6);
vm.stopPrank();
vm.startPrank(ALICE);
deployedToken.approve(manifest.proxy, 1_000e6);
deployedBank.deposit(1_000e6);
deployedBank.withdraw(100e6);
vm.stopPrank();
vm.startPrank(BOB);
deployedToken.approve(manifest.proxy, 500e6);
deployedBank.deposit(500e6);
vm.stopPrank();
}
function _writePublicAnvilManifest() internal returns (string memory path) {
path = string.concat(fixtureDir, "/public-anvil-manifest.json");
vm.writeFile(
path,
string.concat(
'{"schemaVersion":1,"network":"anvil","chainId":31337,"deploymentBlock":1,"rpcUrl":"http://127.0.0.1:8545","token":"',
vm.toString(TOKEN),
'","proxy":"',
vm.toString(PROXY),
'","implementation":"',
vm.toString(IMPLEMENTATION),
'","owner":"',
vm.toString(OWNER),
'","actors":[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}'
)
);
}
function _writeLegacyParallelActorManifest() internal returns (string memory path) {
path = string.concat(fixtureDir, "/legacy-parallel-actors.json");
vm.writeFile(
path,
string.concat(
'{"schemaVersion":1,"network":"anvil","chainId":31337,"deploymentBlock":1,"rpcUrl":"http://127.0.0.1:8545","token":"',
vm.toString(TOKEN),
'","proxy":"',
vm.toString(PROXY),
'","implementation":"',
vm.toString(IMPLEMENTATION),
'","owner":"',
vm.toString(OWNER),
'","actorLabels":["owner","Alice","Bob"],"actors":[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}'
)
);
}
function _writeAnvilManifestWithExtraActorKey(string memory key, string memory value)
internal
returns (string memory path)
{
path = string.concat(fixtureDir, "/extra-actor-key.json");
vm.writeFile(
path,
string.concat(
'{"schemaVersion":1,"network":"anvil","chainId":31337,"deploymentBlock":1,"rpcUrl":"http://127.0.0.1:8545","token":"',
vm.toString(TOKEN),
'","proxy":"',
vm.toString(PROXY),
'","implementation":"',
vm.toString(IMPLEMENTATION),
'","owner":"',
vm.toString(OWNER),
'","actors":[{"label":"owner","address":"',
vm.toString(OWNER),
'","',
key,
'":"',
value,
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}'
)
);
}
function _writePublicBaseManifest() internal returns (string memory path) {
path = string.concat(fixtureDir, "/public-base-manifest.json");
vm.writeFile(
path,
string.concat(
'{"schemaVersion":1,"network":"baseSepolia","chainId":84532,"deploymentBlock":1,"rpcUrl":"https://public.invalid","explorerBaseUrl":"https://sepolia.basescan.org","token":"',
vm.toString(TOKEN),
'","proxy":"',
vm.toString(PROXY),
'","implementation":"',
vm.toString(IMPLEMENTATION),
'","owner":"',
vm.toString(OWNER),
'","actors":[{"label":"Presenter","address":"',
vm.toString(OWNER),
'"},{"label":"Recipient","address":"',
vm.toString(BOB),
'"}]}'
)
);
}
function _etchManifestContracts() internal {
vm.etch(TOKEN, hex"00");
vm.etch(PROXY, hex"00");
vm.etch(IMPLEMENTATION, hex"00");
}
function _actors() internal pure returns (DemoScript.Actor[] memory actors) {
actors = new DemoScript.Actor[](3);
actors[0] = DemoScript.Actor({label: "owner", address_: OWNER});
actors[1] = DemoScript.Actor({label: "Alice", address_: ALICE});
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) {
bytes memory h = bytes(haystack);
bytes memory n = bytes(needle);
if (n.length > h.length) return false;
for (uint256 i; i + n.length <= h.length; ++i) {
bool match_ = true;
for (uint256 j; j < n.length; ++j) {
if (h[i + j] != n[j]) {
match_ = false;
break;
}
}
if (match_) return true;
}
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);
}
}