Commit afcc51a4 authored by Matt Solomon's avatar Matt Solomon Committed by GitHub

test: more blueprint tests (#11782)

* test: more blueprint tests

* address PR feedback
parent ccf9d3e2
...@@ -28,7 +28,7 @@ library Blueprint { ...@@ -28,7 +28,7 @@ library Blueprint {
/// @notice Thrown when parsing a blueprint preamble and the preamble data is not empty. /// @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. /// 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. /// @notice Thrown during deployment if the ERC version is not supported.
error UnsupportedERCVersion(uint8 version); error UnsupportedERCVersion(uint8 version);
...@@ -37,6 +37,9 @@ library Blueprint { ...@@ -37,6 +37,9 @@ library Blueprint {
/// which will deploy a corresponding blueprint contract (with no data section). Based on the /// which will deploy a corresponding blueprint contract (with no data section). Based on the
/// reference implementation in https://eips.ethereum.org/EIPS/eip-5202. /// reference implementation in https://eips.ethereum.org/EIPS/eip-5202.
function blueprintDeployerBytecode(bytes memory _initcode) internal pure returns (bytes memory) { 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 blueprintPreamble = hex"FE7100"; // ERC-5202 preamble.
bytes memory blueprintBytecode = bytes.concat(blueprintPreamble, _initcode); bytes memory blueprintBytecode = bytes.concat(blueprintPreamble, _initcode);
...@@ -89,12 +92,18 @@ library Blueprint { ...@@ -89,12 +92,18 @@ library Blueprint {
return Preamble(ercVersion, preambleData, initcode); 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 /// @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. /// 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_) { function deployFrom(address _target, bytes32 _salt, bytes memory _data) internal returns (address newContract_) {
Preamble memory preamble = parseBlueprintPreamble(address(_target).code); Preamble memory preamble = parseBlueprintPreamble(address(_target).code);
if (preamble.ercVersion != 0) revert UnsupportedERCVersion(preamble.ercVersion); 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); bytes memory initcode = bytes.concat(preamble.initcode, _data);
assembly ("memory-safe") { assembly ("memory-safe") {
...@@ -103,11 +112,6 @@ library Blueprint { ...@@ -103,11 +112,6 @@ library Blueprint {
if (newContract_ == address(0)) revert DeploymentFailed(); 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. /// @notice Convert a bytes array to a uint256.
function bytesToUint(bytes memory _b) internal pure returns (uint256) { function bytesToUint(bytes memory _b) internal pure returns (uint256) {
if (_b.length > 32) revert BytesArrayTooLong(); if (_b.length > 32) revert BytesArrayTooLong();
......
...@@ -4,43 +4,241 @@ pragma solidity 0.8.15; ...@@ -4,43 +4,241 @@ pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol"; import { Test } from "forge-std/Test.sol";
import { Blueprint } from "src/libraries/Blueprint.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 { 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); vm.assume(_initcode.length > 0);
// Convert the initcode to match the ERC-5202 blueprint format. // 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. // Deploy the blueprint.
address blueprintAddress; address blueprintAddress = deployWithCreate2(blueprintInitcode, bytes32(0));
assembly ("memory-safe") {
blueprintAddress := create2(0, add(blueprintInitcode, 0x20), mload(blueprintInitcode), 0)
}
require(blueprintAddress != address(0), "DeployImplementations: create2 failed");
// Read the blueprint code from the deployed code. // Read the blueprint code from the deployed code.
bytes memory blueprintCode = address(blueprintAddress).code; bytes memory blueprintCode = address(blueprintAddress).code;
// Parse the blueprint preamble. // Parse the blueprint preamble and ensure it matches the expected values.
Blueprint.Preamble memory preamble = Blueprint.parseBlueprintPreamble(blueprintCode); Blueprint.Preamble memory preamble = blueprint.parseBlueprintPreamble(blueprintCode);
assertEq(preamble.ercVersion, 0, "100"); assertEq(preamble.ercVersion, 0, "100");
assertEq(preamble.preambleData, hex"", "200"); assertEq(preamble.preambleData, hex"", "200");
assertEq(preamble.initcode, _initcode, "300"); 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. // These test cases (and the logic for bytesToUint) are taken from forge-std.
assertEq(3, Blueprint.bytesToUint(hex"03")); assertEq(3, blueprint.bytesToUint(hex"03"));
assertEq(2, Blueprint.bytesToUint(hex"02")); assertEq(2, blueprint.bytesToUint(hex"02"));
assertEq(255, Blueprint.bytesToUint(hex"ff")); assertEq(255, blueprint.bytesToUint(hex"ff"));
assertEq(29625, Blueprint.bytesToUint(hex"73b9")); assertEq(29625, blueprint.bytesToUint(hex"73b9"));
// Additional test cases. // Additional test cases.
assertEq(0, Blueprint.bytesToUint(hex"")); assertEq(0, blueprint.bytesToUint(hex""));
assertEq(0, Blueprint.bytesToUint(hex"00")); assertEq(0, blueprint.bytesToUint(hex"00"));
assertEq(14545064521499334880, Blueprint.bytesToUint(hex"c9da731e871ad8e0")); assertEq(3, blueprint.bytesToUint(hex"0003"));
assertEq(type(uint256).max, Blueprint.bytesToUint(bytes.concat(bytes32(type(uint256).max)))); 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))));
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment