diff --git a/.gas-snapshot b/.gas-snapshot index 1886e6c34b..e05132231c 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -61,6 +61,8 @@ DateTimeLibTest:testNthWeekdayInMonthOfYearTimestamp() (gas: 12031) DateTimeLibTest:testNthWeekdayInMonthOfYearTimestamp(uint256,uint256,uint256,uint256) (runs: 268, μ: 3460, ~: 3500) DateTimeLibTest:testWeekday() (gas: 705) DateTimeLibTest:test__codesize() (gas: 20080) +DeploylessPredeployQueryerTest:testPredeployQueryer(bytes32) (runs: 268, μ: 215590, ~: 227249) +DeploylessPredeployQueryerTest:test__codesize() (gas: 6836) DynamicBufferLibTest:testClear(uint256) (runs: 268, μ: 171018, ~: 171180) DynamicBufferLibTest:testDynamicBuffer(bytes[],uint256) (runs: 268, μ: 983393, ~: 908603) DynamicBufferLibTest:testDynamicBuffer(uint256) (runs: 268, μ: 945149, ~: 732824) diff --git a/DeploylessPredeployQueryer.yul b/DeploylessPredeployQueryer.yul new file mode 100644 index 0000000000..8844941700 --- /dev/null +++ b/DeploylessPredeployQueryer.yul @@ -0,0 +1,50 @@ +object "DeploylessPredeployQueryer" { + code { + codecopy(returndatasize(), dataoffset("runtime"), codesize()) + let target := mload(returndatasize()) + let targetQueryCalldata := mload(0x20) + let factoryCalldata := mload(0x60) + // If the target does not exist, deploy it. + if iszero(extcodesize(target)) { + if iszero( + call( + gas(), + mload(0x40), + returndatasize(), + add(factoryCalldata, 0x20), + mload(factoryCalldata), + 0x00, + 0x20 + ) + ) { + returndatacopy(0x00, 0x00, returndatasize()) + revert(0x00, returndatasize()) + } + if iszero(and(gt(returndatasize(), 0x1f), eq(mload(0x00), target))) { + mstore(0x00, 0xd1f6b812) // `ReturnedAddressMismatch()`. + revert(0x1c, 0x04) + } + } + if iszero( + call( + gas(), + target, + callvalue(), + add(targetQueryCalldata, 0x20), + mload(targetQueryCalldata), + codesize(), + 0x00 + ) + ) { + returndatacopy(0x00, 0x00, returndatasize()) + revert(0x00, returndatasize()) + } + mstore(0x00, 0x20) + mstore(0x20, returndatasize()) + returndatacopy(0x40, 0x00, returndatasize()) + return(0x00, add(0x60, returndatasize())) + } + object "runtime" { + code {} + } +} diff --git a/README.md b/README.md index fad5828875..2becc4e304 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ utils ├─ SignatureCheckerLib — "Library for verification of ECDSA and ERC1271 signatures" ├─ ECDSA — "Library for verification of ECDSA signatures" ├─ EIP712 — "Contract for EIP-712 typed structured data hashing and signing" +├─ DeploylessPredeployQueryer — "Deployless queryer for predeploys" ├─ ERC1967Factory — "Factory for deploying and managing ERC1967 proxy contracts" ├─ ERC1967FactoryConstants — "The address and bytecode of the canonical ERC1967Factory" ├─ JSONParserLib — "Library for parsing JSONs" diff --git a/src/utils/DeploylessPredeployQueryer.sol b/src/utils/DeploylessPredeployQueryer.sol new file mode 100644 index 0000000000..73b0ddc98f --- /dev/null +++ b/src/utils/DeploylessPredeployQueryer.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Deployless queryer for predeploys. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/DeploylessPredeployQueryer.sol) +/// +/// @dev +/// This contract is not meant to ever actually be deployed, +/// only mock deployed and used via a static `eth_call`. +/// +/// Creation code (hex-encoded): +/// `38607d3d393d5160208051606051833b156045575b506000928391389184825192019034905af115603c578082523d90523d8160403e3d60600190f35b503d81803e3d90fd5b8260008281935190833d9101906040515af11560745783815114601f3d111660145763d1f6b81290526004601cfd5b3d81803e3d90fdfe` +/// See: https://gist.github.com/Vectorized/f77fce00a03dfa99aee526d2a77fd2aa +/// +/// May be useful for generating ERC-6492 compliant signatures. +/// Inspired by Ambire's DeploylessUniversalSigValidator +/// (https://github.com/AmbireTech/signature-validator/blob/main/contracts/DeploylessUniversalSigValidator.sol) +contract DeploylessPredeployQueryer { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The returned address by the factory does not match the provided address. + error ReturnedAddressMismatch(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTRUCTOR */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The code of the deployed contract can be `abi.decoded` into bytes, + /// which can be `abi.decoded` into the required variables. + /// + /// For example, if `targetQueryCalldata` is expected to return a `uint256`, + /// you will use `abi.decode(abi.decode(deployed.code, (bytes)), (uint256))` to + /// get the returned `uint256`. + constructor( + address target, + bytes memory targetQueryCalldata, + address factory, + bytes memory factoryCalldata + ) payable { + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) + // If the target does not exist, deploy it. + if iszero(extcodesize(target)) { + if iszero( + call( + gas(), + factory, + returndatasize(), + add(factoryCalldata, 0x20), + mload(factoryCalldata), + m, + 0x20 + ) + ) { + returndatacopy(m, 0x00, returndatasize()) + revert(m, returndatasize()) + } + if iszero(and(gt(returndatasize(), 0x1f), eq(mload(m), target))) { + mstore(0x00, 0xd1f6b812) // `ReturnedAddressMismatch()`. + revert(0x1c, 0x04) + } + } + if iszero( + call( + gas(), + target, + callvalue(), + add(targetQueryCalldata, 0x20), + mload(targetQueryCalldata), + codesize(), + 0x00 + ) + ) { + returndatacopy(m, 0x00, returndatasize()) + revert(m, returndatasize()) + } + mstore(m, 0x20) + mstore(add(m, 0x20), returndatasize()) + returndatacopy(add(m, 0x40), 0x00, returndatasize()) + return(m, add(0x60, returndatasize())) + } + } +} diff --git a/test/DeploylessPredeployQueryer.t.sol b/test/DeploylessPredeployQueryer.t.sol new file mode 100644 index 0000000000..0d952d3512 --- /dev/null +++ b/test/DeploylessPredeployQueryer.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {LibClone} from "../src/utils/LibClone.sol"; +import {DeploylessPredeployQueryer} from "../src/utils/DeploylessPredeployQueryer.sol"; + +library RandomBytesGeneratorLib { + function generate(uint256 seed) internal pure returns (bytes memory result) { + /// @solidity memory-safe-assembly + assembly { + result := add(0x20, mload(0x40)) + mstore(0x00, seed) + let n := mod(keccak256(0x00, 0x20), 50) + mstore(result, n) + for { let i := 0 } lt(i, n) { i := add(i, 0x20) } { + mstore(0x20, i) + mstore(add(i, add(result, 0x20)), keccak256(0x00, 0x40)) + } + mstore(0x40, add(n, add(result, 0x20))) + } + } + + function next(uint256 seed) internal pure returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, seed) + result := keccak256(0x00, 0x20) + } + } +} + +contract Target { + function generate(uint256 seed) public pure returns (bytes memory result) { + result = RandomBytesGeneratorLib.generate(seed); + /// @solidity memory-safe-assembly + assembly { + mstore(sub(result, 0x20), 0x20) + return(sub(result, 0x20), add(add(0x40, mod(seed, 50)), mload(result))) + } + } + + function next(uint256 seed) public pure returns (uint256 result) { + result = RandomBytesGeneratorLib.next(seed); + } +} + +contract Factory { + address public implementation; + + constructor() { + implementation = address(new Target()); + } + + function deploy(bytes32 salt) public returns (address) { + if (predictDeployment(salt).code.length != 0) return predictDeployment(salt); + return LibClone.cloneDeterministic(implementation, salt); + } + + function predictDeployment(bytes32 salt) public view returns (address) { + return LibClone.predictDeterministicAddress(implementation, salt, address(this)); + } +} + +contract DeploylessPredeployQueryerTest is SoladyTest { + Factory factory; + + bytes internal constant _CREATION_CODE = + hex"38607d3d393d5160208051606051833b156045575b506000928391389184825192019034905af115603c578082523d90523d8160403e3d60600190f35b503d81803e3d90fd5b8260008281935190833d9101906040515af11560745783815114601f3d111660145763d1f6b81290526004601cfd5b3d81803e3d90fdfe"; + + function setUp() public { + factory = new Factory(); + } + + struct _TestTemps { + address target; + uint256 seed; + address deployed; + bytes factoryCalldata; + bytes targetQueryCalldata; + } + + function _deployQuery( + address target, + bytes memory targetQueryCalldata, + bytes memory factoryCalldata + ) internal returns (address result) { + if (_random() % 2 == 0) { + return address( + new DeploylessPredeployQueryer( + target, targetQueryCalldata, address(factory), factoryCalldata + ) + ); + } + bytes memory args = + abi.encode(target, targetQueryCalldata, address(factory), factoryCalldata); + bytes memory initcode; + if (_random() % 2 == 0) { + initcode = _CREATION_CODE; + } else { + initcode = type(DeploylessPredeployQueryer).creationCode; + } + initcode = abi.encodePacked(initcode, args); + /// @solidity memory-safe-assembly + assembly { + result := create(0, add(0x20, initcode), mload(initcode)) + } + } + + function testPredeployQueryer(bytes32 salt) public { + _TestTemps memory t; + t.target = factory.predictDeployment(salt); + if (_random() % 2 == 0) { + assertEq(factory.deploy(salt), t.target); + } + t.factoryCalldata = abi.encodeWithSignature("deploy(bytes32)", salt); + t.seed = _random(); + if (_random() % 2 == 0) { + vm.expectRevert(DeploylessPredeployQueryer.ReturnedAddressMismatch.selector); + address wrongTarget = address(uint160(t.target) ^ 1); + t.deployed = _deployQuery(wrongTarget, t.targetQueryCalldata, t.factoryCalldata); + } + if (_random() % 2 == 0) { + t.targetQueryCalldata = abi.encodeWithSignature("generate(uint256)", t.seed); + t.deployed = _deployQuery(t.target, t.targetQueryCalldata, t.factoryCalldata); + assertEq( + abi.decode(abi.decode(t.deployed.code, (bytes)), (bytes)), + RandomBytesGeneratorLib.generate(t.seed) + ); + } + t.targetQueryCalldata = abi.encodeWithSignature("next(uint256)", t.seed); + t.deployed = _deployQuery(t.target, t.targetQueryCalldata, t.factoryCalldata); + assertEq( + abi.decode(abi.decode(t.deployed.code, (bytes)), (uint256)), + RandomBytesGeneratorLib.next(t.seed) + ); + } +}