From afcc51a4e89d5189930ce5b6701f7ceb0b1db10d Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Fri, 6 Sep 2024 11:07:12 -0700 Subject: [PATCH] test: more blueprint tests (#11782) * test: more blueprint tests * address PR feedback --- .../src/libraries/Blueprint.sol | 18 +- .../test/libraries/Blueprint.t.sol | 236 ++++++++++++++++-- 2 files changed, 228 insertions(+), 26 deletions(-) diff --git a/packages/contracts-bedrock/src/libraries/Blueprint.sol b/packages/contracts-bedrock/src/libraries/Blueprint.sol index 84e41346775a..2e0979e1c6bf 100644 --- a/packages/contracts-bedrock/src/libraries/Blueprint.sol +++ b/packages/contracts-bedrock/src/libraries/Blueprint.sol @@ -28,7 +28,7 @@ library Blueprint { /// @notice Thrown when parsing a blueprint preamble and the preamble data is not empty. /// We do not use the preamble data, so it's expected to be empty. - error UnexpectedPreambleData(); + error UnexpectedPreambleData(bytes data); /// @notice Thrown during deployment if the ERC version is not supported. error UnsupportedERCVersion(uint8 version); @@ -37,6 +37,9 @@ library Blueprint { /// which will deploy a corresponding blueprint contract (with no data section). Based on the /// reference implementation in https://eips.ethereum.org/EIPS/eip-5202. function blueprintDeployerBytecode(bytes memory _initcode) internal pure returns (bytes memory) { + // Check that the initcode is not empty. + if (_initcode.length == 0) revert EmptyInitcode(); + bytes memory blueprintPreamble = hex"FE7100"; // ERC-5202 preamble. bytes memory blueprintBytecode = bytes.concat(blueprintPreamble, _initcode); @@ -89,12 +92,18 @@ library Blueprint { return Preamble(ercVersion, preambleData, initcode); } + /// @notice Parses the code at the given `_target` as a blueprint and deploys the resulting initcode. + /// This version of `deployFrom` is used when the initcode requires no constructor arguments. + function deployFrom(address _target, bytes32 _salt) internal returns (address) { + return deployFrom(_target, _salt, new bytes(0)); + } + /// @notice Parses the code at the given `_target` as a blueprint and deploys the resulting initcode /// with the given `_data` appended, i.e. `_data` is the ABI-encoded constructor arguments. function deployFrom(address _target, bytes32 _salt, bytes memory _data) internal returns (address newContract_) { Preamble memory preamble = parseBlueprintPreamble(address(_target).code); if (preamble.ercVersion != 0) revert UnsupportedERCVersion(preamble.ercVersion); - if (preamble.preambleData.length != 0) revert UnexpectedPreambleData(); + if (preamble.preambleData.length != 0) revert UnexpectedPreambleData(preamble.preambleData); bytes memory initcode = bytes.concat(preamble.initcode, _data); assembly ("memory-safe") { @@ -103,11 +112,6 @@ library Blueprint { if (newContract_ == address(0)) revert DeploymentFailed(); } - /// @notice Parses the code at the given `_target` as a blueprint and deploys the resulting initcode. - function deployFrom(address _target, bytes32 _salt) internal returns (address) { - return deployFrom(_target, _salt, new bytes(0)); - } - /// @notice Convert a bytes array to a uint256. function bytesToUint(bytes memory _b) internal pure returns (uint256) { if (_b.length > 32) revert BytesArrayTooLong(); diff --git a/packages/contracts-bedrock/test/libraries/Blueprint.t.sol b/packages/contracts-bedrock/test/libraries/Blueprint.t.sol index 419e68f95666..94a30ab99fb0 100644 --- a/packages/contracts-bedrock/test/libraries/Blueprint.t.sol +++ b/packages/contracts-bedrock/test/libraries/Blueprint.t.sol @@ -4,43 +4,241 @@ pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; import { Blueprint } from "src/libraries/Blueprint.sol"; +// Used to test that constructor args are appended properly when deploying from a blueprint. +contract ConstructorArgMock { + uint256 public x; + bytes public y; + + constructor(uint256 _x, bytes memory _y) { + x = _x; + y = _y; + } +} + +// Foundry cheatcodes operate on the next call, and since all library methods are internal we would +// just JUMP to them if called directly in the test. Therefore we wrap the library in a contract. +contract BlueprintHarness { + function blueprintDeployerBytecode(bytes memory _initcode) public pure returns (bytes memory) { + return Blueprint.blueprintDeployerBytecode(_initcode); + } + + function parseBlueprintPreamble(bytes memory _bytecode) public pure returns (Blueprint.Preamble memory) { + return Blueprint.parseBlueprintPreamble(_bytecode); + } + + function deployFrom(address _blueprint, bytes32 _salt) public returns (address) { + return Blueprint.deployFrom(_blueprint, _salt); + } + + function deployFrom(address _blueprint, bytes32 _salt, bytes memory _args) public returns (address) { + return Blueprint.deployFrom(_blueprint, _salt, _args); + } + + function bytesToUint(bytes memory _bytes) public pure returns (uint256) { + return Blueprint.bytesToUint(_bytes); + } +} + contract Blueprint_Test is Test { - // TODO add tests that things revert if an address has no code. + BlueprintHarness blueprint; + + function setUp() public { + blueprint = new BlueprintHarness(); + } + + function deployWithCreate2(bytes memory _initcode, bytes32 _salt) public returns (address addr_) { + assembly ("memory-safe") { + addr_ := create2(0, add(_initcode, 0x20), mload(_initcode), _salt) + } + require(addr_ != address(0), "deployWithCreate2: deployment failed"); + } + + // --- We start with the test cases from ERC-5202 --- + + // An example (and trivial!) blueprint contract with no data section, whose initcode is just the STOP instruction. + function test_ERC5202_trivialBlueprint_succeeds() public view { + bytes memory bytecode = hex"FE710000"; + Blueprint.Preamble memory preamble = blueprint.parseBlueprintPreamble(bytecode); + + assertEq(preamble.ercVersion, 0, "100"); + assertEq(preamble.preambleData, hex"", "200"); + assertEq(preamble.initcode, hex"00", "300"); + } + + // An example blueprint contract whose initcode is the trivial STOP instruction and whose data + // section contains the byte 0xFF repeated seven times. + function test_ERC5202_blueprintWithDataSection_succeeds() public view { + // Here, 0xFE71 is the magic header, 0x01 means version 0 + 1 length bit, 0x07 encodes the + // length in bytes of the data section. These are followed by the data section, and then the + // initcode. For illustration, this code with delimiters would be: + // 0xFE71|01|07|FFFFFFFFFFFFFF|00 + bytes memory bytecode = hex"FE710107FFFFFFFFFFFFFF00"; + Blueprint.Preamble memory preamble = blueprint.parseBlueprintPreamble(bytecode); + + assertEq(preamble.ercVersion, 0, "100"); + assertEq(preamble.preambleData, hex"FFFFFFFFFFFFFF", "200"); + assertEq(preamble.initcode, hex"00", "300"); + } + + // An example blueprint whose initcode is the trivial STOP instruction and whose data section + // contains the byte 0xFF repeated 256 times. + function test_ERC5202_blueprintWithLargeDataSection_succeeds() public view { + // Delimited, this would be 0xFE71|02|0100|FF...FF|00 + bytes memory bytecode = + hex"FE71020100FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00"; + Blueprint.Preamble memory preamble = blueprint.parseBlueprintPreamble(bytecode); + + assertEq(preamble.ercVersion, 0, "100"); + assertEq(preamble.preambleData.length, 256, "200"); + for (uint256 i = 0; i < 256; i++) { + assertEq(preamble.preambleData[i], bytes1(0xFF), string.concat("300-", vm.toString(i))); + } + assertEq(preamble.initcode, hex"00", "400"); + } - function test_roundtrip_succeeds(bytes memory _initcode) public { + // --- Now we add a generic roundtrip test --- + + // Test that a roundtrip from initcode to blueprint to initcode succeeds, i.e. the invariant + // here is that `parseBlueprintPreamble(blueprintDeployerBytecode(x)) = x`. + function testFuzz_roundtrip_succeeds(bytes memory _initcode) public { vm.assume(_initcode.length > 0); // Convert the initcode to match the ERC-5202 blueprint format. - bytes memory blueprintInitcode = Blueprint.blueprintDeployerBytecode(_initcode); + bytes memory blueprintInitcode = blueprint.blueprintDeployerBytecode(_initcode); // Deploy the blueprint. - address blueprintAddress; - assembly ("memory-safe") { - blueprintAddress := create2(0, add(blueprintInitcode, 0x20), mload(blueprintInitcode), 0) - } - require(blueprintAddress != address(0), "DeployImplementations: create2 failed"); + address blueprintAddress = deployWithCreate2(blueprintInitcode, bytes32(0)); // Read the blueprint code from the deployed code. bytes memory blueprintCode = address(blueprintAddress).code; - // Parse the blueprint preamble. - Blueprint.Preamble memory preamble = Blueprint.parseBlueprintPreamble(blueprintCode); + // Parse the blueprint preamble and ensure it matches the expected values. + Blueprint.Preamble memory preamble = blueprint.parseBlueprintPreamble(blueprintCode); assertEq(preamble.ercVersion, 0, "100"); assertEq(preamble.preambleData, hex"", "200"); assertEq(preamble.initcode, _initcode, "300"); } - function test_bytesToUint_succeeds() public pure { + // --- Lastly, function-specific unit tests --- + + function test_blueprintDeployerBytecode_emptyInitcode_reverts() public { + bytes memory initcode = ""; + vm.expectRevert(Blueprint.EmptyInitcode.selector); + blueprint.blueprintDeployerBytecode(initcode); + } + + function test_parseBlueprintPreamble_notABlueprint_reverts() public { + // Length too short. + bytes memory invalidBytecode = hex"01"; + vm.expectRevert(Blueprint.NotABlueprint.selector); + blueprint.parseBlueprintPreamble(invalidBytecode); + + // First byte is not 0xFE. + invalidBytecode = hex"0071"; + vm.expectRevert(Blueprint.NotABlueprint.selector); + blueprint.parseBlueprintPreamble(invalidBytecode); + + // Second byte is not 0x71. + invalidBytecode = hex"FE00"; + vm.expectRevert(Blueprint.NotABlueprint.selector); + blueprint.parseBlueprintPreamble(invalidBytecode); + } + + function test_parseBlueprintPreamble_reservedBitsSet_reverts() public { + bytes memory invalidBytecode = hex"FE7103"; + vm.expectRevert(Blueprint.ReservedBitsSet.selector); + blueprint.parseBlueprintPreamble(invalidBytecode); + } + + function test_parseBlueprintPreamble_emptyInitcode_reverts() public { + bytes memory invalidBytecode = hex"FE7100"; + vm.expectRevert(Blueprint.EmptyInitcode.selector); + blueprint.parseBlueprintPreamble(invalidBytecode); + } + + function testFuzz_deployFrom_succeeds(bytes memory _initcode, bytes32 _salt) public { + vm.assume(_initcode.length > 0); + vm.assume(_initcode[0] != 0xef); // https://eips.ethereum.org/EIPS/eip-3541 + + // This deployBytecode prefix is the same bytecode used in `blueprintDeployerBytecode`, and + // it ensures that whatever initcode the fuzzer generates is actually deployable. + bytes memory deployBytecode = bytes.concat(hex"61", bytes2(uint16(_initcode.length)), hex"3d81600a3d39f3"); + bytes memory initcode = bytes.concat(deployBytecode, _initcode); + bytes memory blueprintInitcode = blueprint.blueprintDeployerBytecode(initcode); + + // Deploy the blueprint. + address blueprintAddress = deployWithCreate2(blueprintInitcode, _salt); + + // Deploy from the blueprint. + address deployedContract = Blueprint.deployFrom(blueprintAddress, _salt); + + // Verify the deployment worked. + assertTrue(deployedContract != address(0), "100"); + assertTrue(deployedContract.code.length > 0, "200"); + assertEq(keccak256(deployedContract.code), keccak256(_initcode), "300"); + } + + // Here we deploy a simple mock contract to test that constructor args are appended properly. + function testFuzz_deployFrom_withConstructorArgs_succeeds(uint256 _x, bytes memory _y, bytes32 _salt) public { + bytes memory blueprintInitcode = blueprint.blueprintDeployerBytecode(type(ConstructorArgMock).creationCode); + + // Deploy the blueprint. + address blueprintAddress = deployWithCreate2(blueprintInitcode, _salt); + + // Deploy from the blueprint. + bytes memory args = abi.encode(_x, _y); + address deployedContract = blueprint.deployFrom(blueprintAddress, _salt, args); + + // Verify the deployment worked. + assertTrue(deployedContract != address(0), "100"); + assertTrue(deployedContract.code.length > 0, "200"); + assertEq(keccak256(deployedContract.code), keccak256(type(ConstructorArgMock).runtimeCode), "300"); + assertEq(ConstructorArgMock(deployedContract).x(), _x, "400"); + assertEq(ConstructorArgMock(deployedContract).y(), _y, "500"); + } + + function test_deployFrom_unsupportedERCVersion_reverts() public { + bytes32 salt = bytes32(0); + address blueprintAddress = makeAddr("blueprint"); + + bytes memory invalidBlueprintCode = hex"FE710400"; // ercVersion = uint8(0x04 & 0xfc) >> 2 = 1 + vm.etch(blueprintAddress, invalidBlueprintCode); + vm.expectRevert(abi.encodeWithSelector(Blueprint.UnsupportedERCVersion.selector, 1)); + blueprint.deployFrom(blueprintAddress, salt); + + invalidBlueprintCode = hex"FE71B000"; // ercVersion = uint8(0xB0 & 0xfc) >> 2 = 44 + vm.etch(blueprintAddress, invalidBlueprintCode); + vm.expectRevert(abi.encodeWithSelector(Blueprint.UnsupportedERCVersion.selector, 44)); + blueprint.deployFrom(blueprintAddress, salt); + } + + function test_deployFrom_unexpectedPreambleData_reverts() public { + bytes32 salt = bytes32(0); + address blueprintAddress = makeAddr("blueprint"); + + // Create invalid blueprint code with non-empty preamble data + bytes memory invalidBlueprintCode = hex"FE7101030102030001020304"; + vm.etch(blueprintAddress, invalidBlueprintCode); + + // Expect revert with UnexpectedPreambleData error + vm.expectRevert(abi.encodeWithSelector(Blueprint.UnexpectedPreambleData.selector, hex"010203")); + blueprint.deployFrom(blueprintAddress, salt); + } + + function test_bytesToUint_succeeds() public view { // These test cases (and the logic for bytesToUint) are taken from forge-std. - assertEq(3, Blueprint.bytesToUint(hex"03")); - assertEq(2, Blueprint.bytesToUint(hex"02")); - assertEq(255, Blueprint.bytesToUint(hex"ff")); - assertEq(29625, Blueprint.bytesToUint(hex"73b9")); + assertEq(3, blueprint.bytesToUint(hex"03")); + assertEq(2, blueprint.bytesToUint(hex"02")); + assertEq(255, blueprint.bytesToUint(hex"ff")); + assertEq(29625, blueprint.bytesToUint(hex"73b9")); // Additional test cases. - assertEq(0, Blueprint.bytesToUint(hex"")); - assertEq(0, Blueprint.bytesToUint(hex"00")); - assertEq(14545064521499334880, Blueprint.bytesToUint(hex"c9da731e871ad8e0")); - assertEq(type(uint256).max, Blueprint.bytesToUint(bytes.concat(bytes32(type(uint256).max)))); + assertEq(0, blueprint.bytesToUint(hex"")); + assertEq(0, blueprint.bytesToUint(hex"00")); + assertEq(3, blueprint.bytesToUint(hex"0003")); + assertEq(3145731, blueprint.bytesToUint(hex"300003")); + assertEq(14545064521499334880, blueprint.bytesToUint(hex"c9da731e871ad8e0")); + assertEq(14545064521499334880, blueprint.bytesToUint(hex"00c9da731e871ad8e0")); + assertEq(type(uint256).max, blueprint.bytesToUint(bytes.concat(bytes32(type(uint256).max)))); } }