Files
uupl-smart-contract/test/ScriptPreflight.t.sol
T

500 lines
20 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.35;
import {Test} from "forge-std/Test.sol";
import {DemoScript} from "../script/lib/DemoScript.sol";
import {DeployV1} from "../script/DeployV1.s.sol";
import {SeedV1Demo} from "../script/SeedV1Demo.s.sol";
import {CheckState} from "../script/CheckState.s.sol";
import {BankV1} from "../src/BankV1.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 ScriptPreflightTest is Test {
ScriptPreflightHarness internal harness;
string internal fixtureDir;
address internal constant OWNER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
address internal constant ALICE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
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[3] memory rejected = [uint256(1), uint256(8453), 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 testBaseManifestRequiresOnlyOwnerActor() 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 testBaseSepoliaManifestUsesCamelCaseAndOmitsUnavailableUrls() public {
string memory path = _writePublicBaseManifest();
vm.chainId(84532);
_etchManifestContracts();
DemoScript.Manifest memory manifest = harness.readManifest(path, true);
assertEq(manifest.network, "baseSepolia");
assertEq(manifest.rpcUrl, "");
assertEq(manifest.explorerBaseUrl, "");
assertEq(manifest.actors[0].label, "owner");
assertEq(manifest.actors[0].address_, OWNER);
}
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 testSeedV1DemoExecutesExactActOneStateAndCheckStateAcceptsIt() public {
(string memory path, DemoScript.Manifest memory manifest) = _deployFixture();
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 testCheckStateRejectsManifestImplementationMismatch() public {
(string memory path, DemoScript.Manifest memory manifest) = _deployFixture();
vm.writeJson(string.concat('"', vm.toString(manifest.token), '"'), path, ".implementation");
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path);
vm.setEnv("DEMO_EXPECTED_STAGE", "deployed");
CheckState checker = new CheckState();
vm.expectRevert(
abi.encodeWithSelector(
DemoScript.UnexpectedAddress.selector, "implementation", manifest.token, manifest.implementation
)
);
checker.run();
}
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) {
path = string.concat(fixtureDir, "/deployed.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 _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,"token":"',
vm.toString(TOKEN),
'","proxy":"',
vm.toString(PROXY),
'","implementation":"',
vm.toString(IMPLEMENTATION),
'","owner":"',
vm.toString(OWNER),
'","actors":[{"label":"owner","address":"',
vm.toString(OWNER),
'"}]}'
)
);
}
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 _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;
}
}