From 8730fbfe74a1634ccc050ee23aedf7fb5fcb1945 Mon Sep 17 00:00:00 2001 From: howydev Date: Fri, 19 Jan 2024 16:14:51 -0500 Subject: [PATCH] feat: add everything --- .editorconfig | 19 +++ .env.example | 11 ++ .github/workflows/ci.yml | 92 +++++++++++ .gitignore | 20 +++ .gitmodules | 9 ++ .gitpod.yml | 14 ++ .prettierignore | 17 ++ .prettierrc.yml | 7 + .solhint.json | 14 ++ .vscode/settings.json | 9 ++ LICENSE.md | 16 ++ README.md | 33 ++++ bun.lockb | Bin 0 -> 28807 bytes foundry.toml | 53 +++++++ lib/forge-std | 1 + lib/openzeppelin-contracts | 1 + lib/solady | 1 + package.json | 39 +++++ remappings.txt | 3 + script/SafeDeployPatternDemo.s.sol | 62 ++++++++ src/SingleKeccakCreate3.sol | 199 ++++++++++++++++++++++++ src/SingleKeccakCreate3Proxy.sol | 99 ++++++++++++ src/interfaces/ISingleKeccakCreate3.sol | 7 + src/mocks/MockERC20.sol | 46 ++++++ src/mocks/MockSingleKeccakCreate3.sol | 56 +++++++ test/GasComparison.t.sol | 77 +++++++++ test/SingleKeccakCreate3Test.t.sol | 87 +++++++++++ 27 files changed, 992 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .gitpod.yml create mode 100644 .prettierignore create mode 100644 .prettierrc.yml create mode 100644 .solhint.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 foundry.toml create mode 160000 lib/forge-std create mode 160000 lib/openzeppelin-contracts create mode 160000 lib/solady create mode 100644 package.json create mode 100644 remappings.txt create mode 100644 script/SafeDeployPatternDemo.s.sol create mode 100644 src/SingleKeccakCreate3.sol create mode 100644 src/SingleKeccakCreate3Proxy.sol create mode 100644 src/interfaces/ISingleKeccakCreate3.sol create mode 100644 src/mocks/MockERC20.sol create mode 100644 src/mocks/MockSingleKeccakCreate3.sol create mode 100644 test/GasComparison.t.sol create mode 100644 test/SingleKeccakCreate3Test.t.sol diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..746ae31 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.sol] +indent_size = 4 + +[*.tree] +indent_size = 1 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..98c1028 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY" +export API_KEY_ARBISCAN="YOUR_API_KEY_ARBISCAN" +export API_KEY_BSCSCAN="YOUR_API_KEY_BSCSCAN" +export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" +export API_KEY_GNOSISSCAN="YOUR_API_KEY_GNOSISSCAN" +export API_KEY_INFURA="YOUR_API_KEY_INFURA" +export API_KEY_OPTIMISTIC_ETHERSCAN="YOUR_API_KEY_OPTIMISTIC_ETHERSCAN" +export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" +export API_KEY_SNOWTRACE="YOUR_API_KEY_SNOWTRACE" +export MNEMONIC="YOUR_MNEMONIC" +export FOUNDRY_PROFILE="default" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7550749 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +name: "CI" + +env: + API_KEY_ALCHEMY: ${{ secrets.API_KEY_ALCHEMY }} + FOUNDRY_PROFILE: "ci" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - "main" + +jobs: + lint: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + - name: "Lint the code" + run: "bun run lint" + + - name: "Add lint summary" + run: | + echo "## Lint result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + build: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + - name: "Build the contracts and print their size" + run: "forge build --sizes" + + - name: "Add build summary" + run: | + echo "## Build result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test: + needs: ["lint", "build"] + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + - name: "Show the Foundry config" + run: "forge config" + + - name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance" + run: > + echo "FOUNDRY_FUZZ_SEED=$( + echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) + )" >> $GITHUB_ENV + + - name: "Run the tests" + run: "forge test" + + - name: "Add test summary" + run: | + echo "## Tests result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e108b40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# directories +cache +coverage +node_modules +out + +# files +*.env +*.log +.DS_Store +.pnp.* +lcov.info +package-lock.json +pnpm-lock.yaml +yarn.lock + +# broadcasts +!broadcast +broadcast/* +broadcast/*/31337/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..da1251c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/Vectorized/solady +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..b9646d8 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,14 @@ +image: "gitpod/workspace-bun" + +tasks: + - name: "Install dependencies" + before: | + curl -L https://foundry.paradigm.xyz | bash + source ~/.bashrc + foundryup + init: "bun install" + +vscode: + extensions: + - "esbenp.prettier-vscode" + - "NomicFoundation.hardhat-solidity" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3996d20 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,17 @@ +# directories +broadcast +cache +coverage +node_modules +out + +# files +*.env +*.log +.DS_Store +.pnp.* +bun.lockb +lcov.info +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..a1ecdbb --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,7 @@ +bracketSpacing: true +printWidth: 120 +proseWrap: "always" +singleQuote: false +tabWidth: 2 +trailingComma: "all" +useTabs: false diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..7a15ca0 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,14 @@ +{ + "extends": "solhint:recommended", + "rules": { + "code-complexity": ["error", 8], + "compiler-version": ["error", ">=0.8.23"], + "func-name-mixedcase": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], + "max-line-length": ["error", 120], + "named-parameters-mapping": "warn", + "no-console": "off", + "not-rely-on-time": "off", + "one-contract-per-file": "off" + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..241108b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "[solidity]": { + "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" + }, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + }, + "solidity.formatter": "forge" +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..88a2b87 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2023 Paul Razvan Berg + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d48ca2 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Single Keccak Create3 + +This is a different implementation of `CREATE3` that only requires a single keccak256 hash to calculate deployed address. + +## How it works + +To achieve this, before deploying a proxy, we `SSTORE2.write` the final deployed bytecode from the deployer and store a pointer to the storage contract in the deployer. The proxies deployed from the deployer will do a `SSTORE2.read` on `CALLER.pointer` to get the final deployed bytecode and return that in its constructor. + +We use a different SSTORE2 implementation here, using CREATE2 instead of CREATE under the hood. This bumps up costs slightly for the first deployment, but on the 2nd+ deployment of the same contract we get significant savings. + +Because of how this works, contract constructors are never run. It's highly recommended for constructor logic to be in an `initializer` function instead. See the section on safety below. + +## Gas + +Gas estimates with Solady's CREATE3 are in [GasComparison.t.sol](./test/GasComparison.t.sol). Disclaimer: Gas values are measured using foundry and will be inaccurate so this should only be used as a ballpark estimate. + +Cost of deploying an ERC20: +Single Keccak Create3, cached contract + no storage: 636k +Solady CREATE3: 732k +Single Keccak Create3, cached contract + storage: 810k +Single Keccak Create3, new contract + no storage: 1.26m +Single Keccak Create3, new contract + storage: 1.44m + +## Safety + +Deployments using this proxy bypass all constructor logic and is significantly less safe than regular deployments. The danger here can largely be mitigated via adding tests to foundry deploy scripts - see example in [SafeDeployPatternDemo](./script/SafeDeployPatternDemo.s.sol). + +## Acknowledgements + Future Extensions + +A significant amount of assembly logic was lifted from [Solady](https://github.com/Vectorized/solady). + +1. Utilizing SSTORE or TSTORE instead of another SSTORE2 for `storageArgs` +2. We currently cache deployed bytecode, this means deploying a second contract a different immutable variable wouldn't count as 2nd+ deployment. Can consider doing it like how solidity constructors set up immutable vars and have the proxy take in arrays of memory arguments and memory locations of immutable args \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..204be629ba773796c7f5e6039f60fe58079ca484 GIT binary patch literal 28807 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.8.23 <0.9.0; + +import { MockSingleKeccakCreate3 } from "../src/mocks/MockSingleKeccakCreate3.sol"; +import { SingleKeccakCreate3 } from "../src/SingleKeccakCreate3.sol"; +import { MockERC20 } from "../src/mocks/MockERC20.sol"; +import { Script, console2 } from "forge-std/Script.sol"; +import { Test } from "forge-std/Test.sol"; + +/** + * @title Safe Deploy Demo. Steps: + * @dev 1. if salt is used + * @dev 2. deploys an instance locally (this sets up immutable arguments), then copies that bytecode + * @dev 3. does checks on expected constructor logic (total supply, balances) + */ +contract SafeDeployPatternDemo is Script, SingleKeccakCreate3, Test { + function run() public { + // Replace these vars before actually deploying + bytes32 salt = 0x0; + address deployerAddr = address(0); + MockERC20 expected = new MockERC20(5); + uint256 mintAmt = 5; + address owner = address(0); + + bytes memory runtimeCode = address(expected).code; + + // gasless salt test + if (_getDeployedAddress(salt, deployerAddr).code.length > 0) { + console2.log("Salt already used"); + return; + } + + MockSingleKeccakCreate3 skc3 = MockSingleKeccakCreate3(deployerAddr); + + vm.startBroadcast(); + + bytes32[] memory storageSlots = new bytes32[](2); + storageSlots[0] = bytes32(keccak256(abi.encodePacked(bytes32(uint256(uint160(owner))), bytes32(uint256(0))))); + storageSlots[1] = bytes32(uint256(2)); + + bytes32[] memory storageVals = new bytes32[](2); + storageVals[0] = bytes32(mintAmt); + storageVals[1] = bytes32(mintAmt); + + MockERC20 deployed = MockERC20(skc3.deployAndSetupStorage(salt, runtimeCode, storageVals, storageSlots, 0)); + + vm.stopBroadcast(); + + // Add tests on the deployed contract + if (address(deployed).code.length == 0) { + revert("Deployment failed"); + } + + if (deployed.balanceOf(owner) != 5) { + revert("Mint failed"); + } + + if (deployed.totalSupply() != 5) { + revert("Total supply failed"); + } + } +} diff --git a/src/SingleKeccakCreate3.sol b/src/SingleKeccakCreate3.sol new file mode 100644 index 0000000..dc267d0 --- /dev/null +++ b/src/SingleKeccakCreate3.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { CREATE3 } from "@solady/src/utils/CREATE3.sol"; +import { Create2 } from "@openzeppelin-contracts/contracts/utils/Create2.sol"; +import { SingleKeccakCreate3Proxy } from "./SingleKeccakCreate3Proxy.sol"; +import { ISingleKeccakCreate3 } from "./interfaces/ISingleKeccakCreate3.sol"; + +/** + * @title SingleKeccakCreate3 + * @notice CREATE3 that requires a single keccak256 hash operation to mine + * @dev 4 deploy modes - no storage, storage, no storage with init calls, storage with init calls + */ +abstract contract SingleKeccakCreate3 is ISingleKeccakCreate3 { + /** + * @dev 1st byte: bool hasStorage + * @dev last 20 bytes: address pointer + */ + bytes32 public setupInfo; + bytes32 public storageArgs; + + uint256 internal constant _DATA_OFFSET = 1; + + error DeploymentFailed(); + error AlreadyDeployed(); + error CallFailed(uint256 idx); + error LengthMismatch(); + + /// Private helper methods + + /** + * @notice Private helper function to set up an SSTORE2 pointer + * @dev some logic lifted from solady SSTORE2. Uses create2 instead of create + * @param data contract creation code + */ + function _setupPointer(bytes memory data) private returns (bytes32 addr) { + assembly { + let originalDataLength := mload(data) + let dataSize := add(originalDataLength, _DATA_OFFSET) + mstore( + // Do a out-of-gas revert if `dataSize` is more than 2 bytes. + // The actual EVM limit may be smaller and may change over time. + add(data, gt(dataSize, 0xffff)), + // Left shift `dataSize` by 64 so that it lines up with the 0000 after PUSH2. + or(0xfd61000080600a3d393df300, shl(0x40, dataSize)) + ) + + dataSize := add(dataSize, 0xa) + let dataStart := add(data, 0x15) + + // FMP: 00000000_00000000_000000FF_20BYTESADDRESS + // FMP+32: salt = 0 + // FMP+64: initBytecodeHash + let fmp := mload(0x40) + mstore(add(fmp, 0x40), keccak256(dataStart, dataSize)) + mstore(fmp, or(shl(160, 0xff), address())) + addr := shr(96, shl(96, keccak256(add(fmp, 0x0b), 85))) + + // if no code at address, deploy + if iszero(extcodesize(addr)) { + let deployedAddr := create2(0, dataStart, dataSize, returndatasize()) + if iszero(deployedAddr) { + mstore(0x00, 0x30116425) + revert(0x1c, 0x04) + } + } + + // Restore original length of the variable size `data`. + mstore(data, originalDataLength) + } + } + + /** + * Private helper function to make calls to deployed contracts + * @param target contract to call + * @param calldatas array of calldata for calls + * @param values array of values for calls + */ + function _call(address target, bytes[] calldata calldatas, uint256[] calldata values) private { + if (calldatas.length != values.length) { + revert LengthMismatch(); + } + + uint256 len = calldatas.length; + for (uint256 i = 0; i < len; i++) { + (bool success,) = target.call{ value: values[i] }(calldatas[i]); + if (!success) { + revert CallFailed(i); + } + } + } + + function _getDeployedAddress(bytes32 salt, address deployer) internal pure returns (address addr) { + bytes memory proxy = type(SingleKeccakCreate3Proxy).creationCode; + assembly { + // layout : + // ptr: 00000000_00000000_000000FF_20BYTESADDRESS + // ptr+32: salt = 0 + // ptr+64: initBytecodeHash + let ptr := mload(0x40) + mstore(add(ptr, 0x40), keccak256(add(proxy, 0x20), mload(proxy))) + mstore(add(ptr, 0x20), salt) + mstore(ptr, or(shl(160, 0xff), deployer)) + addr := keccak256(add(ptr, 0x0b), 85) + } + } + + /// Internal Functions + + /** + * @notice Deploy a contract using single keccak create3 + * @param salt salt for create3 + * @param creationCode contract creation code + * @param value value to send + */ + function _deploy(bytes32 salt, bytes calldata creationCode, uint256 value) internal returns (address) { + // hasStorage = false + setupInfo = _setupPointer(creationCode); + + return address(new SingleKeccakCreate3Proxy{ salt: salt, value: value }()); + } + + /** + * @notice Deploy a contract using single keccak create3 then perform specified calls + * @param salt salt for create3 + * @param creationCode contract creation code + * @param calldatas array of calldata for calls + * @param values array of values for calls + * @param value value to send + */ + function _deployAndCall( + bytes32 salt, + bytes calldata creationCode, + bytes[] calldata calldatas, + uint256[] calldata values, + uint256 value + ) + internal + returns (address deployed) + { + deployed = _deploy(salt, creationCode, value); + _call(deployed, calldatas, values); + } + + /** + * @notice Deploy a contract using single keccak create3, setup storage + * @param salt salt for create3 + * @param creationCode contract creation code + * @param storageArgsVals array of bytes32 storage arguments + * @param storageArgsLoc array of storage locations to save data into + * @param value value to send + */ + function _deployAndSetupStorage( + bytes32 salt, + bytes calldata creationCode, + bytes32[] calldata storageArgsVals, + bytes32[] calldata storageArgsLoc, + uint256 value + ) + internal + returns (address) + { + if (storageArgsVals.length != storageArgsLoc.length) { + revert LengthMismatch(); + } + + // hasStorage = true + setupInfo = _setupPointer(creationCode) | bytes32(uint256(1) << 255); + storageArgs = _setupPointer(abi.encode(storageArgsVals, storageArgsLoc)); + + return address(new SingleKeccakCreate3Proxy{ salt: salt, value: value }()); + } + + /** + * @notice Deploy a contract using single keccak create3, setup storage, then perform specified calls + * @param salt salt for create3 + * @param creationCode contract creation code + * @param storageArgsVals array of bytes32 storage arguments + * @param storageArgsLoc array of storage locations to save data into + * @param calldatas array of calldata for calls + * @param values array of values for calls + * @param value value to send + */ + function _deployAndSetupStorageAndCall( + bytes32 salt, + bytes calldata creationCode, + bytes32[] calldata storageArgsVals, + bytes32[] calldata storageArgsLoc, + bytes[] calldata calldatas, + uint256[] calldata values, + uint256 value + ) + internal + returns (address deployed) + { + deployed = _deployAndSetupStorage(salt, creationCode, storageArgsVals, storageArgsLoc, value); + _call(deployed, calldatas, values); + } +} diff --git a/src/SingleKeccakCreate3Proxy.sol b/src/SingleKeccakCreate3Proxy.sol new file mode 100644 index 0000000..382aa25 --- /dev/null +++ b/src/SingleKeccakCreate3Proxy.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { ISingleKeccakCreate3 } from "./interfaces/ISingleKeccakCreate3.sol"; + +contract SingleKeccakCreate3Proxy { + uint256 private constant _DATA_OFFSET = 1; + + error InvalidPointer(); + + /** + * // Equivalent ~solidity code + * + * constructor() payable { + * bytes32 p = ISingleKeccakCreate3(msg.sender).setupInfo(); + * bytes memory creationCode = SSTORE2.read(address(uint160(uint256(p)))); + * + * p = p >> 255; + * if (uint256(p) % 2 == 1) { + * (bytes32[] memory storageArgsVals, bytes32[] memory storageArgsLoc) = abi.decode( + * SSTORE2.read(address(uint160(uint256(ISingleKeccakCreate3(msg.sender).storageArgs())))), + * (bytes32[], bytes32[]) + * ); + * + * for (uint256 i = 0; i < storageArgsVals.length; i++) { + * assembly { + * sstore( + * mload(add(storageArgsLoc, add(0x20, mul(i, 0x20)))), + * mload(add(storageArgsVals, add(0x20, mul(i, 0x20)))) + * ) + * } + * } + * } + * + * assembly { + * return(add(creationCode, 0x20), mload(creationCode)) + * } + * } + */ + + constructor() payable { + assembly { + // "setupInfo()" + mstore(0x00, 0xc0c8e36f) + pop(call(gas(), caller(), 0, 28, 0x04, 0, 0x20)) + let ptr := mload(0) + let pointerCodeSize := extcodesize(ptr) + if iszero(pointerCodeSize) { + // Store the function selector of `InvalidPointer()`. + mstore(0x00, 0x11052bb4) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + + let size := sub(pointerCodeSize, _DATA_OFFSET) + + let bytecode := mload(0x40) + mstore(0x40, add(bytecode, and(add(size, 0x3f), 0xffe0))) + mstore(bytecode, size) + mstore(add(add(bytecode, 0x20), size), 0) // Zeroize the last slot. + extcodecopy(ptr, add(bytecode, 0x20), _DATA_OFFSET, size) + + // Set up storage if hasStorage flag is set + if iszero(iszero(shr(255, ptr))) { + // "storageArgs()" + mstore(0x00, 0x5601376b) + pop(call(gas(), caller(), 0, 28, 0x04, 0, 0x20)) + ptr := mload(0) + pointerCodeSize := extcodesize(ptr) + if iszero(pointerCodeSize) { + // Store the function selector of `InvalidPointer()`. + mstore(0x00, 0x11052bb4) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + + size := sub(pointerCodeSize, _DATA_OFFSET) + + let storageArgs := mload(0x40) + mstore(0x40, add(storageArgs, and(add(size, 0x3f), 0xffe0))) + mstore(storageArgs, size) + mstore(add(add(storageArgs, 0x20), size), 0) // Zeroize the last slot. + extcodecopy(ptr, add(storageArgs, 0x20), _DATA_OFFSET, size) + + let valsOffset := mload(add(storageArgs, 0x20)) + let locOffset := mload(add(storageArgs, 0x40)) + + let valsArrStart := add(storageArgs, 0x60) + let len := mload(valsArrStart) + let locArrStart := add(add(valsArrStart, mul(len, 0x20)), 0x20) + + for { ptr := mul(len, 0x20) } ptr { ptr := sub(ptr, 0x20) } { + sstore(mload(add(locArrStart, ptr)), mload(add(valsArrStart, ptr))) + } + } + return(add(bytecode, 0x20), mload(bytecode)) + } + } +} diff --git a/src/interfaces/ISingleKeccakCreate3.sol b/src/interfaces/ISingleKeccakCreate3.sol new file mode 100644 index 0000000..9ee7ff0 --- /dev/null +++ b/src/interfaces/ISingleKeccakCreate3.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +interface ISingleKeccakCreate3 { + function setupInfo() external view returns (bytes32); + function storageArgs() external view returns (bytes32); +} diff --git a/src/mocks/MockERC20.sol b/src/mocks/MockERC20.sol new file mode 100644 index 0000000..f8f961a --- /dev/null +++ b/src/mocks/MockERC20.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { ERC20 } from "@openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { ShortString, ShortStrings } from "@openzeppelin-contracts/contracts/utils/ShortStrings.sol"; +import { Ownable } from "@openzeppelin-contracts/contracts/access/Ownable.sol"; + +/** + * @title MockERC20 + * @dev is ownable, uses immutables for ERC20 name()/symbol(), and does mint on creation + */ +contract MockERC20 is ERC20, Ownable { + /** + * Storage Layout: + * balances in slot 0 + * totalSupply in slot 2 + * name in slot 3 + * symbol in slot 4 + * owner in slot 5 + */ + + using ShortStrings for string; + using ShortStrings for ShortString; + + ShortString internal immutable $name; + ShortString internal immutable $symbol; + + function name() public view override returns (string memory) { + return $name.toString(); + } + + function symbol() public view override returns (string memory) { + return $symbol.toString(); + } + + constructor(uint256 amt) ERC20("", "") Ownable(msg.sender) { + $name = string("testErc20").toShortString(); + $symbol = string("testErc20").toShortString(); + + _mint(msg.sender, amt); + } + + function mint(address to, uint256 amt) external onlyOwner { + _mint(to, amt); + } +} diff --git a/src/mocks/MockSingleKeccakCreate3.sol b/src/mocks/MockSingleKeccakCreate3.sol new file mode 100644 index 0000000..a34745c --- /dev/null +++ b/src/mocks/MockSingleKeccakCreate3.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { SingleKeccakCreate3 } from "../SingleKeccakCreate3.sol"; + +contract MockSingleKeccakCreate3 is SingleKeccakCreate3 { + function deploy(bytes32 salt, bytes calldata data, uint256 value) external returns (address addr) { + return _deploy(salt, data, value); + } + + function deployAndCall( + bytes32 salt, + bytes calldata data, + bytes[] calldata calldatas, + uint256[] calldata values, + uint256 value + ) + external + returns (address addr) + { + return _deployAndCall(salt, data, calldatas, values, value); + } + + function deployAndSetupStorage( + bytes32 salt, + bytes calldata creationCode, + bytes32[] calldata storageArgsVals, + bytes32[] calldata storageArgsLoc, + uint256 value + ) + external + returns (address) + { + return _deployAndSetupStorage(salt, creationCode, storageArgsVals, storageArgsLoc, value); + } + + function deployAndSetupStorageAndCall( + bytes32 salt, + bytes calldata creationCode, + bytes32[] calldata storageArgsVals, + bytes32[] calldata storageArgsLoc, + bytes[] calldata calldatas, + uint256[] calldata values, + uint256 value + ) + external + returns (address) + { + return + _deployAndSetupStorageAndCall(salt, creationCode, storageArgsVals, storageArgsLoc, calldatas, values, value); + } + + function getDeployedAddress(bytes32 salt) external view returns (address) { + return _getDeployedAddress(salt, address(this)); + } +} diff --git a/test/GasComparison.t.sol b/test/GasComparison.t.sol new file mode 100644 index 0000000..7d0e6f8 --- /dev/null +++ b/test/GasComparison.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { MockSingleKeccakCreate3 } from "../src/mocks/MockSingleKeccakCreate3.sol"; +import { MockERC20 } from "../src/mocks/MockERC20.sol"; +import { CREATE3 } from "@solady/src/utils/CREATE3.sol"; + +contract GasComparisonTest is Test { + uint256 mintAmt = 5; + MockERC20 public mockErc20; // has been deployed by deployer + MockSingleKeccakCreate3 public deployer; + + function setUp() public { + deployer = new MockSingleKeccakCreate3(); + + bytes32[] memory storageSlots = new bytes32[](1); + storageSlots[0] = bytes32(uint256(5)); + bytes32[] memory storageVals = new bytes32[](1); + storageVals[0] = bytes32(uint256(uint160(address(this)))); + + mockErc20 = MockERC20( + deployer.deployAndSetupStorage(0, address(new MockERC20(mintAmt)).code, storageVals, storageSlots, 0) + ); + } + + function test_soladyCreate3() public { + bytes memory creationCode = abi.encodePacked(type(MockERC20).creationCode, mintAmt); + CREATE3.deploy(0, creationCode, 0); + } + + function test_singleKeccakCreate3() public { + // mutate storage args a little to force a sstore2 instance + bytes32[] memory storageSlots = new bytes32[](3); + storageSlots[0] = + bytes32(keccak256(abi.encodePacked(bytes32(uint256(uint160(address(this)))), bytes32(uint256(0))))); + storageSlots[1] = bytes32(uint256(2)); + storageSlots[2] = bytes32(uint256(5)); + + bytes32[] memory storageVals = new bytes32[](3); + storageVals[0] = bytes32(uint256(mintAmt)); + storageVals[1] = bytes32(uint256(mintAmt)); + storageVals[2] = bytes32(uint256(uint160(address(this)))); + + // mutate the code a little to force a sstore2 instance + deployer.deployAndSetupStorage( + bytes32(uint256(1)), abi.encodePacked(address(mockErc20).code, uint256(1)), storageVals, storageSlots, 0 + ); + } + + function test_singleKeccakCreate3_NoStorage() public { + // mutate the code a little to force a sstore2 instance + deployer.deploy(bytes32(uint256(1)), abi.encodePacked(address(mockErc20).code, uint256(1)), 0); + } + + function test_singleKeccakCreate3_Repeat() public { + // mutate storage args a little to force a sstore2 instance + bytes32[] memory storageSlots = new bytes32[](3); + storageSlots[0] = + bytes32(keccak256(abi.encodePacked(bytes32(uint256(uint160(address(this)))), bytes32(uint256(0))))); + storageSlots[1] = bytes32(uint256(2)); + storageSlots[2] = bytes32(uint256(5)); + + bytes32[] memory storageVals = new bytes32[](3); + storageVals[0] = bytes32(uint256(mintAmt)); + storageVals[1] = bytes32(uint256(mintAmt)); + storageVals[2] = bytes32(uint256(uint160(address(this)))); + + // no sstore2 instance here + deployer.deployAndSetupStorage(bytes32(uint256(1)), address(mockErc20).code, storageVals, storageSlots, 0); + } + + function test_singleKeccakCreate3_NoStorage_Repeat() public { + // no sstore2 instance here + deployer.deploy(bytes32(uint256(1)), address(mockErc20).code, 0); + } +} diff --git a/test/SingleKeccakCreate3Test.t.sol b/test/SingleKeccakCreate3Test.t.sol new file mode 100644 index 0000000..40ee347 --- /dev/null +++ b/test/SingleKeccakCreate3Test.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { MockSingleKeccakCreate3 } from "../src/mocks/MockSingleKeccakCreate3.sol"; +import { MockERC20 } from "../src/mocks/MockERC20.sol"; + +contract SingleKeccakCreate3Test is Test { + MockERC20 public mockErc20; + + MockSingleKeccakCreate3 public deployer; + + function setUp() public { + deployer = new MockSingleKeccakCreate3(); + mockErc20 = new MockERC20(5); + } + + function test_flagsInPointer() public { + bytes32[] memory bytes32Arr = new bytes32[](1); + + // no storage, no immutable + deployer.deploy(0, address(mockErc20).code, 0); + assertEq((uint256(deployer.setupInfo() >> 255)), 0); + + // has storage + deployer.deployAndSetupStorage(bytes32(uint256(2)), address(mockErc20).code, bytes32Arr, bytes32Arr, 0); + assertEq((uint256(deployer.setupInfo() >> 255)), 1); + assertTrue(uint256(deployer.storageArgs()) >= 1); + } + + function test_deployedHasFunctionality() public { + MockERC20 a = MockERC20(deployer.deploy(0, address(mockErc20).code, 0)); + + assertEq(address(a).code.length, address(mockErc20).code.length); + assertEq(a.name(), "testErc20"); + assertEq(a.symbol(), "testErc20"); + assertEq(a.balanceOf(address(this)), 0); + } + + function test_matchDeployedBytecode() public { + // address.code gives runtime code with immutables already set + MockERC20 a = MockERC20(deployer.deploy(0, address(mockErc20).code, 0)); + + assertEq((address(mockErc20).code).length, (address(a).code).length); + assertEq(address(mockErc20).code, address(a).code); + } + + function test_getDeployedAddress(bytes32 r) public { + address a = deployer.getDeployedAddress(r); + address b = deployer.deploy(r, address(mockErc20).code, 0); + + assertEq(a, b); + } + + function test_noStorageOrImmutableVars() public { + // "creation code" with default values for immutables + MockERC20 a = new MockERC20(0); + + MockERC20 b = MockERC20(deployer.deploy(0, address(a).code, 0)); + + assertEq(b.totalSupply(), 0); + assertEq(b.balanceOf(address(this)), 0); + } + + function test_storageVars(uint128 mintAmt) public { + bytes32[] memory storageSlots = new bytes32[](3); + storageSlots[0] = + bytes32(keccak256(abi.encodePacked(bytes32(uint256(uint160(address(this)))), bytes32(uint256(0))))); + storageSlots[1] = bytes32(uint256(2)); + storageSlots[2] = bytes32(uint256(5)); + + bytes32[] memory storageVals = new bytes32[](3); + storageVals[0] = bytes32(uint256(mintAmt)); + storageVals[1] = bytes32(uint256(mintAmt)); + storageVals[2] = bytes32(uint256(uint160(address(this)))); + + MockERC20 b = + MockERC20(deployer.deployAndSetupStorage(0, address(mockErc20).code, storageVals, storageSlots, 0)); + + assertEq(b.totalSupply(), uint256(mintAmt)); + assertEq(b.balanceOf(address(this)), uint256(mintAmt)); + + b.mint(address(this), uint256(mintAmt)); // is owner, so can mint + assertEq(b.totalSupply(), uint256(mintAmt) * 2); + assertEq(b.balanceOf(address(this)), uint256(mintAmt) * 2); + } +}