Skip to content

Commit

Permalink
test: more blueprint tests (#11782)
Browse files Browse the repository at this point in the history
* test: more blueprint tests

* address PR feedback
  • Loading branch information
mds1 authored Sep 6, 2024
1 parent ccf9d3e commit afcc51a
Show file tree
Hide file tree
Showing 2 changed files with 228 additions and 26 deletions.
18 changes: 11 additions & 7 deletions packages/contracts-bedrock/src/libraries/Blueprint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -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") {
Expand All @@ -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();
Expand Down
236 changes: 217 additions & 19 deletions packages/contracts-bedrock/test/libraries/Blueprint.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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))));
}
}

0 comments on commit afcc51a

Please sign in to comment.