From 3d9c062f4d1099b446649d54bad1643240a654e7 Mon Sep 17 00:00:00 2001 From: rahul0eth <63620041+rahul0eth@users.noreply.github.com> Date: Tue, 24 Oct 2023 22:23:38 -0500 Subject: [PATCH 1/3] refactor: update UserNonces test: add DeployTwice, DeployAndUpdateNonce, getBytecodeHash tests --- README.md | 8 +- ...eDeployer.sol => PredictiveDeployer.s.sol} | 0 src/PredictiveDeployer.sol | 40 +++++++-- src/interfaces/IPredictiveDeployer.sol | 7 +- test/PredictiveDeployer.t.sol | 88 ++++++++++++++++--- 5 files changed, 120 insertions(+), 23 deletions(-) rename script/{PredictiveDeployer.sol => PredictiveDeployer.s.sol} (100%) diff --git a/README.md b/README.md index a6e34d9..3510ccd 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ WE MANDATE THAT ONLY SIGNER AND XSAFE CAN SUBMIT SIGNATURES FOR DEPLOYMENT DEPLOYMENT REQUIRES SIGNATURE, NOT ADDRESS DIRECTLY -- SALT IS A FUNCTION OF ADDRESS - ANY ONE CAN SUBMIT AN ADDRESS. ONLY PRIVATE KEY HOLDER CAN SUBMIT SIGNATURE +- SALT IS A FUNCTION OF ADDRESS - ANYONE CAN SUBMIT AN ADDRESS. ONLY PRIVATE KEYHOLDER CAN SUBMIT SIGNATURE - WITH SIGNATURE METHOD, THE ONLY WAY TO OBTAIN A SALT, TIED TO AN ADDRESS, IS TO OWN THAT ADDRESS' PRIVATE KEY - ONLY RIGHTFUL OWNERS CAN DEPLOY THEIR CONTRACTS AT THEIR DETERMINISTIC ADDRESSES + +USER NONCE IS DEPENDENT ON THE BYTECODE OF THE FUNCTION +- RATIONALE: USER DOES NOT NEED TO TRACK THE ORDER IN WHICH THEY DEPLOY CONTRACTS A, B, C... ON CHAIN 1 | B, A, C... ON CHAIN 2 +- IN AN EFFORT TO KEEP THE SIGNABLE TRANSACTION HASH UNIQUE FOR EACH TRANSACTION, BYTECODE IS INCLUDED IN THE HAS +- WE CHOSE NOT TO INCLUDE BYTECODE IN THE SALT BECAUSE CREATE2 IS A HASH OF THE BYTECODE AND THE SALT. + - EVEN IF THE SALT IS THE SAME FOR CONTRACT A AND B, THEY WILL HAVE DIFFERENT BYTECODES AND THUS DIFFERENT ADDRESSES. \ No newline at end of file diff --git a/script/PredictiveDeployer.sol b/script/PredictiveDeployer.s.sol similarity index 100% rename from script/PredictiveDeployer.sol rename to script/PredictiveDeployer.s.sol diff --git a/src/PredictiveDeployer.sol b/src/PredictiveDeployer.sol index a753437..10fc603 100644 --- a/src/PredictiveDeployer.sol +++ b/src/PredictiveDeployer.sol @@ -16,7 +16,9 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { bytes32 internal domainSeparator; // Factory Storage - mapping(address => uint256) public userNonces; + //mapping(address => uint256) public userNonces; + mapping(address => mapping(bytes32 => uint256)) public userNonces; + mapping(address => address[]) public deploymentHistory; // Events event Deploy(address indexed principal, address indexed child, bytes32 hashedBytecode, uint256 nonce); @@ -52,8 +54,12 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { * @param _nonce The principal account's current nonce on this factory contract. * @return messageHash The expected signed message hash. */ - function _computeMessageHash(address _principal, uint256 _nonce) internal view returns (bytes32) { - bytes32 txHash = getTransactionHash(_principal, _nonce); + function _computeMessageHash(address _principal, bytes memory _bytecode, uint256 _nonce) + internal + view + returns (bytes32) + { + bytes32 txHash = getTransactionHash(_principal, _bytecode, _nonce); return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); } @@ -63,8 +69,12 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { * @param _nonce The principal account's current nonce on this factory contract. * @return txHash The unique deployment transaction hash to be signed. */ - function getTransactionHash(address _principal, uint256 _nonce) public view returns (bytes32) { - return keccak256(abi.encodePacked(domainSeparator, _principal, _nonce)); + function getTransactionHash(address _principal, bytes memory _bytecode, uint256 _nonce) + public + view + returns (bytes32) + { + return keccak256(abi.encodePacked(domainSeparator, _principal, _bytecode, _nonce)); } /** @@ -74,11 +84,20 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { * @return child The predicted address of the contract to be deployed. */ function getAddress(address _principal, bytes memory _bytecode) public view returns (address) { - uint256 salt = uint256(uint160(_principal)) + userNonces[_principal]; + uint256 salt = uint256(uint160(_principal)) + userNonces[_principal][keccak256(_bytecode)]; bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(_bytecode))); return address(uint160(uint256(hash))); } + /** + * @dev Returns the keccak256 hash of the provided bytecode. + * @param _bytecode The bytecode of the contract to be deployed. + * @return hash The keccak256 hash of the provided bytecode. + */ + function getBytecodeHash(bytes memory _bytecode) public pure returns (bytes32) { + return keccak256(_bytecode); + } + /** * @dev Deploys arbitrary child contracts at predictable addresses, derived from account signatures. * @param _principal The address of the account that signed the message hash. @@ -87,8 +106,8 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { * @return child The address of the deployed contract. */ function deploy(address _principal, bytes memory _signature, bytes memory _bytecode) public returns (address) { - uint256 currentNonce = userNonces[_principal]; - bytes32 expectedMessageHash = _computeMessageHash(_principal, currentNonce); + uint256 currentNonce = userNonces[_principal][keccak256(_bytecode)]; + bytes32 expectedMessageHash = _computeMessageHash(_principal, _bytecode, currentNonce); // Recover principal address address recoveredSigner = ECDSA.recover(expectedMessageHash, _signature); @@ -100,7 +119,7 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { uint256 salt = uint256(uint160(_principal)) + currentNonce; // Update nonce state - userNonces[_principal]++; + userNonces[_principal][keccak256(_bytecode)]++; // Deploy contract address child; @@ -109,6 +128,9 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { if iszero(extcodesize(child)) { revert(0, 0) } } + // Update deployment history + deploymentHistory[_principal].push(child); + emit Deploy(_principal, child, keccak256(_bytecode), currentNonce); return child; } diff --git a/src/interfaces/IPredictiveDeployer.sol b/src/interfaces/IPredictiveDeployer.sol index 25e7353..c192e2b 100644 --- a/src/interfaces/IPredictiveDeployer.sol +++ b/src/interfaces/IPredictiveDeployer.sol @@ -2,8 +2,11 @@ pragma solidity ^0.8.21; interface IPredictiveDeployer { - function userNonces(address _principal) external returns (uint256); - function getTransactionHash(address _principal, uint256 _nonce) external returns (bytes32); + function userNonces(address _principal, bytes32 _hashedBytecode) external returns (uint256); + function getTransactionHash(address _principal, bytes memory _bytecode, uint256 _nonce) + external + returns (bytes32); + function getBytecodeHash(bytes memory _bytecode) external pure returns (bytes32); function getAddress(address _principal, bytes memory _bytecode) external returns (address); function deploy(address _principal, bytes memory _signature, bytes memory _bytecode) external returns (address); } diff --git a/test/PredictiveDeployer.t.sol b/test/PredictiveDeployer.t.sol index df0de82..134f3c9 100644 --- a/test/PredictiveDeployer.t.sol +++ b/test/PredictiveDeployer.t.sol @@ -16,6 +16,7 @@ contract PredictiveDeployerTest is Test { ERC1967Proxy public proxy; Child public child; bytes public childBytecode; + bytes32 public hashedChildBytecode; // Events event Deploy(address indexed sender, address indexed child, bytes32 hashedBytecode, uint256 nonce); @@ -29,16 +30,18 @@ contract PredictiveDeployerTest is Test { // Get Bytecode bytes memory bytecode = type(Child).creationCode; childBytecode = abi.encodePacked(bytecode, abi.encode(address(this))); + hashedChildBytecode = keccak256(childBytecode); } function testFuzz_GetAddress(uint256 pkNum, address sender) public { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr); + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); // Get signature information - bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce); + bytes32 txHash = + IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); @@ -66,10 +69,11 @@ contract PredictiveDeployerTest is Test { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr); + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); // Get signature information - bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce); + bytes32 txHash = + IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); @@ -90,14 +94,42 @@ contract PredictiveDeployerTest is Test { assertEq(actualChild, expectedChild); } - function test_CannotDeployReplay(uint256 pkNum) public { + // helper function + function deployChild(VmSafe.Wallet memory wallet, bytes memory bytecode, bytes32 hashedBytecode) + internal + returns (address) + { + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecode); + bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, bytecode, currentNonce); + bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + return IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, bytecode); + } + + function testFuzz_DeployTwice(uint256 pkNum) public { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr); + // First Deployment + address actualChild1 = deployChild(wallet, childBytecode, hashedChildBytecode); + + // Second Deployment + address actualChild2 = deployChild(wallet, childBytecode, hashedChildBytecode); + + // Assertions + require(actualChild1 != actualChild2, "Addresses should not be equal"); + } + + function testFuzz_CannotDeployReplay(uint256 pkNum) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); + + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); // Get signature information - bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce); + bytes32 txHash = + IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); @@ -112,17 +144,18 @@ contract PredictiveDeployerTest is Test { IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); } - function test_CannotDeployWithoutApproval(uint256 pkNum, address invalidPrincipal) public { + function testFuzz_CannotDeployWithoutApproval(uint256 pkNum, address invalidPrincipal) public { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); // Failing condition: principal is not the signer vm.assume(invalidPrincipal != wallet.addr); - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr); + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); // Get signature information - bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce); + bytes32 txHash = + IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); @@ -134,7 +167,40 @@ contract PredictiveDeployerTest is Test { IPredictiveDeployer(address(proxy)).deploy(invalidPrincipal, signature, childBytecode); } - function test_Receive(uint256 transferAmount) public { + function testFuzz_DeployAndUpdateNonce(uint256 pkNum) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); + + // Get initial nonce + uint256 initialNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); + + // Deploy + address actualChild = deployChild(wallet, childBytecode, hashedChildBytecode); + + // Get updated nonce + uint256 updatedNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); + + // Assertions + require(actualChild != address(0), "Deployment failed"); + require(updatedNonce == initialNonce + 1, "Nonce did not update correctly"); + } + + function testFuzz_getBytecodeHash(bytes memory _childBytecode) public { + if (_childBytecode.length == 0) { + return; // Skip the test for empty bytecode + } + + // Act + bytes32 actualHash = IPredictiveDeployer(address(proxy)).getBytecodeHash(_childBytecode); + + // Expected + bytes32 expectedHash = keccak256(_childBytecode); + + // Assertions + assertEq(actualHash, expectedHash, "Hash mismatch"); + } + + function testFuzz_Receive(uint256 transferAmount) public { // Assumptions vm.assume(transferAmount != 0 && transferAmount <= 1000 ether); From 21b6a706959f2f593500d13c2d09e26cd9505f0a Mon Sep 17 00:00:00 2001 From: cucupac <46691282+cucupac@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:26:44 -0500 Subject: [PATCH 2/3] refactor: refactor test directory, add deploy-order-independence test, address requested changes --- src/PredictiveDeployer.sol | 9 +- test/PredictiveDeployer.admin.t.sol | 15 +--- test/PredictiveDeployer.t.sol | 130 +++++++++++----------------- test/{ => common}/Child.t.sol | 10 ++- test/common/TestSetup.t.sol | 37 ++++++++ test/helpers/DeploymentHelper.t.sol | 26 ++++++ test/test_proxy/ERC1967Proxy.t.sol | 16 +--- 7 files changed, 133 insertions(+), 110 deletions(-) rename test/{ => common}/Child.t.sol (70%) create mode 100644 test/common/TestSetup.t.sol create mode 100644 test/helpers/DeploymentHelper.t.sol diff --git a/src/PredictiveDeployer.sol b/src/PredictiveDeployer.sol index 10fc603..98a3d35 100644 --- a/src/PredictiveDeployer.sol +++ b/src/PredictiveDeployer.sol @@ -16,7 +16,6 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { bytes32 internal domainSeparator; // Factory Storage - //mapping(address => uint256) public userNonces; mapping(address => mapping(bytes32 => uint256)) public userNonces; mapping(address => address[]) public deploymentHistory; @@ -66,6 +65,7 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { /** * @dev Returns the unique deployment transaction hash to be signed. * @param _principal The address of the account for whom this factory contract will deploy a child contract. + * @param _bytecode The bytecode of the contract to be deployed. * @param _nonce The principal account's current nonce on this factory contract. * @return txHash The unique deployment transaction hash to be signed. */ @@ -106,7 +106,8 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { * @return child The address of the deployed contract. */ function deploy(address _principal, bytes memory _signature, bytes memory _bytecode) public returns (address) { - uint256 currentNonce = userNonces[_principal][keccak256(_bytecode)]; + bytes32 hashedByteCode = keccak256(_bytecode); + uint256 currentNonce = userNonces[_principal][hashedByteCode]; bytes32 expectedMessageHash = _computeMessageHash(_principal, _bytecode, currentNonce); // Recover principal address @@ -119,7 +120,7 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { uint256 salt = uint256(uint160(_principal)) + currentNonce; // Update nonce state - userNonces[_principal][keccak256(_bytecode)]++; + userNonces[_principal][hashedByteCode]++; // Deploy contract address child; @@ -131,7 +132,7 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { // Update deployment history deploymentHistory[_principal].push(child); - emit Deploy(_principal, child, keccak256(_bytecode), currentNonce); + emit Deploy(_principal, child, hashedByteCode, currentNonce); return child; } diff --git a/test/PredictiveDeployer.admin.t.sol b/test/PredictiveDeployer.admin.t.sol index f889598..dbe0302 100644 --- a/test/PredictiveDeployer.admin.t.sol +++ b/test/PredictiveDeployer.admin.t.sol @@ -6,21 +6,12 @@ import { PredictiveDeployer } from "../src/PredictiveDeployer.sol"; import { ERC1967Proxy } from "../src/dependencies/proxy/ERC1967Proxy.sol"; import { IERC20 } from "../src/dependencies/token/interfaces/IERC20.sol"; import { IPredictiveDeployerAdmin } from "../src/interfaces/IPredictiveDeployerAdmin.sol"; -import { CONTRACT_DEPLOYER, TEST_ERC20_TOKEN } from "./common/constants.t.sol"; +import { TestSetup } from "./common/TestSetup.t.sol"; +import { CONTRACT_DEPLOYER, TEST_ERC20_TOKEN } from "./common/Constants.t.sol"; -contract PredictiveDeployerTest is Test { +contract PredictiveDeployerTest is TestSetup { /* solhint-disable func-name-mixedcase */ - PredictiveDeployer public implementation; - ERC1967Proxy public proxy; - - function setUp() public { - // Instantiate contracts - implementation = new PredictiveDeployer(); - vm.prank(CONTRACT_DEPLOYER); - proxy = new ERC1967Proxy(address(implementation), abi.encodeWithSignature("initialize()")); - } - function test_ExtractNative(uint256 amount) public { // Setup vm.assume(amount > 0 && amount < 1e22); diff --git a/test/PredictiveDeployer.t.sol b/test/PredictiveDeployer.t.sol index 134f3c9..80e9c83 100644 --- a/test/PredictiveDeployer.t.sol +++ b/test/PredictiveDeployer.t.sol @@ -1,47 +1,26 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.21; -import { Test } from "forge-std/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; import { PredictiveDeployer } from "../src/PredictiveDeployer.sol"; import { ERC1967Proxy } from "../src/dependencies/proxy/ERC1967Proxy.sol"; -import { Child } from "./Child.t.sol"; +import { TestSetup } from "./common/TestSetup.t.sol"; +import { DeploymentHelper } from "./helpers/DeploymentHelper.t.sol"; import { IPredictiveDeployer } from "../src/interfaces/IPredictiveDeployer.sol"; -import { CONTRACT_DEPLOYER } from "./common/constants.t.sol"; +import { CONTRACT_DEPLOYER } from "./common/Constants.t.sol"; -contract PredictiveDeployerTest is Test { +contract PredictiveDeployerTest is DeploymentHelper, TestSetup { /* solhint-disable func-name-mixedcase */ - PredictiveDeployer public implementation; - ERC1967Proxy public proxy; - Child public child; - bytes public childBytecode; - bytes32 public hashedChildBytecode; - - // Events - event Deploy(address indexed sender, address indexed child, bytes32 hashedBytecode, uint256 nonce); - - function setUp() public { - implementation = new PredictiveDeployer(); - vm.prank(CONTRACT_DEPLOYER); - proxy = new ERC1967Proxy(address(implementation), abi.encodeWithSignature("initialize()")); - child = new Child(address(this)); - - // Get Bytecode - bytes memory bytecode = type(Child).creationCode; - childBytecode = abi.encodePacked(bytecode, abi.encode(address(this))); - hashedChildBytecode = keccak256(childBytecode); - } - function testFuzz_GetAddress(uint256 pkNum, address sender) public { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA); // Get signature information bytes32 txHash = - IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce); + IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildA, currentNonce); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); @@ -52,13 +31,13 @@ contract PredictiveDeployerTest is Test { // Expectation vm.startPrank(sender); uint256 snapShot = vm.snapshot(); - address expectedChild = IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); + address expectedChild = IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA); // Set chain state to what it was before the deployment vm.revertTo(snapShot); // Act - address actualChild = IPredictiveDeployer(address(proxy)).getAddress(wallet.addr, childBytecode); + address actualChild = IPredictiveDeployer(address(proxy)).getAddress(wallet.addr, bytecodeChildA); vm.stopPrank(); // Assertions @@ -69,11 +48,11 @@ contract PredictiveDeployerTest is Test { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); + uint256 preDeployNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA); // Get signature information bytes32 txHash = - IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce); + IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildA, preDeployNonce); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); @@ -82,29 +61,18 @@ contract PredictiveDeployerTest is Test { // Expectations vm.startPrank(sender); - address expectedChild = IPredictiveDeployer(address(proxy)).getAddress(wallet.addr, childBytecode); + address expectedChild = IPredictiveDeployer(address(proxy)).getAddress(wallet.addr, bytecodeChildA); vm.expectEmit(true, true, true, true, address(proxy)); - emit Deploy(wallet.addr, expectedChild, keccak256(childBytecode), currentNonce); + emit Deploy(wallet.addr, expectedChild, keccak256(bytecodeChildA), preDeployNonce); // Act - address actualChild = IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); + address actualChild = IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA); + uint256 postDeployNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA); vm.stopPrank(); // Assertions assertEq(actualChild, expectedChild); - } - - // helper function - function deployChild(VmSafe.Wallet memory wallet, bytes memory bytecode, bytes32 hashedBytecode) - internal - returns (address) - { - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecode); - bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, bytecode, currentNonce); - bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); - bytes memory signature = abi.encodePacked(r, s, v); - return IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, bytecode); + assertEq(postDeployNonce, preDeployNonce + 1); } function testFuzz_DeployTwice(uint256 pkNum) public { @@ -112,24 +80,46 @@ contract PredictiveDeployerTest is Test { VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); // First Deployment - address actualChild1 = deployChild(wallet, childBytecode, hashedChildBytecode); + address actualChild1 = deployChild(address(proxy), wallet, bytecodeChildA); // Second Deployment - address actualChild2 = deployChild(wallet, childBytecode, hashedChildBytecode); + address actualChild2 = deployChild(address(proxy), wallet, bytecodeChildA); // Assertions - require(actualChild1 != actualChild2, "Addresses should not be equal"); + assertTrue(actualChild1 != address(0) && actualChild2 != address(0)); + assertTrue(actualChild1 != actualChild2); + } + + function testFuzz_DeployOrderIndependence(uint256 pkNum) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); + + // First deployment set + uint256 snapShot = vm.snapshot(); + address setOneChildA = deployChild(address(proxy), wallet, bytecodeChildA); + address setOneChildB = deployChild(address(proxy), wallet, bytecodeChildB); + + // Set chain state to what it was before first deployment set + vm.revertTo(snapShot); + + // Second deployment set (reverse order) + address setTwoChildB = deployChild(address(proxy), wallet, bytecodeChildB); + address setTwoChildA = deployChild(address(proxy), wallet, bytecodeChildA); + + // Assertions + assertEq(setOneChildA, setTwoChildA); + assertEq(setOneChildB, setTwoChildB); } function testFuzz_CannotDeployReplay(uint256 pkNum) public { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA); // Get signature information bytes32 txHash = - IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce); + IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildA, currentNonce); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); @@ -137,11 +127,11 @@ contract PredictiveDeployerTest is Test { bytes memory signature = abi.encodePacked(r, s, v); // Deploy once - IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); + IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA); // Act: attempt replay vm.expectRevert(PredictiveDeployer.Unauthorized.selector); - IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode); + IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA); } function testFuzz_CannotDeployWithoutApproval(uint256 pkNum, address invalidPrincipal) public { @@ -151,11 +141,11 @@ contract PredictiveDeployerTest is Test { // Failing condition: principal is not the signer vm.assume(invalidPrincipal != wallet.addr); - uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); + uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA); // Get signature information bytes32 txHash = - IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce); + IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildA, currentNonce); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); @@ -164,31 +154,11 @@ contract PredictiveDeployerTest is Test { // Act: attempt with invalid principal vm.expectRevert(PredictiveDeployer.Unauthorized.selector); - IPredictiveDeployer(address(proxy)).deploy(invalidPrincipal, signature, childBytecode); - } - - function testFuzz_DeployAndUpdateNonce(uint256 pkNum) public { - // Setup - VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); - - // Get initial nonce - uint256 initialNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); - - // Deploy - address actualChild = deployChild(wallet, childBytecode, hashedChildBytecode); - - // Get updated nonce - uint256 updatedNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode); - - // Assertions - require(actualChild != address(0), "Deployment failed"); - require(updatedNonce == initialNonce + 1, "Nonce did not update correctly"); + IPredictiveDeployer(address(proxy)).deploy(invalidPrincipal, signature, bytecodeChildA); } - function testFuzz_getBytecodeHash(bytes memory _childBytecode) public { - if (_childBytecode.length == 0) { - return; // Skip the test for empty bytecode - } + function testFuzz_GetBytecodeHash(bytes memory _childBytecode) public { + vm.assume(_childBytecode.length > 0 && _childBytecode.length <= 24576); // max contract size // Act bytes32 actualHash = IPredictiveDeployer(address(proxy)).getBytecodeHash(_childBytecode); @@ -197,7 +167,7 @@ contract PredictiveDeployerTest is Test { bytes32 expectedHash = keccak256(_childBytecode); // Assertions - assertEq(actualHash, expectedHash, "Hash mismatch"); + assertEq(actualHash, expectedHash, "Hash mismatch."); } function testFuzz_Receive(uint256 transferAmount) public { diff --git a/test/Child.t.sol b/test/common/Child.t.sol similarity index 70% rename from test/Child.t.sol rename to test/common/Child.t.sol index d38e6f5..f0b54ec 100644 --- a/test/Child.t.sol +++ b/test/common/Child.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.21; -contract Child { +contract ChildA { uint256 public number; address public admin; @@ -14,3 +14,11 @@ contract Child { number++; } } + +contract ChildB { + uint256 public number; + + function increment() public { + number++; + } +} diff --git a/test/common/TestSetup.t.sol b/test/common/TestSetup.t.sol new file mode 100644 index 0000000..ce21051 --- /dev/null +++ b/test/common/TestSetup.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { IPredictiveDeployer } from "../../src/interfaces/IPredictiveDeployer.sol"; +import { PredictiveDeployer } from "../../src/PredictiveDeployer.sol"; +import { ERC1967Proxy } from "../../src/dependencies/proxy/ERC1967Proxy.sol"; +import { ChildA, ChildB } from "./Child.t.sol"; +import { CONTRACT_DEPLOYER } from "./../common/Constants.t.sol"; + +abstract contract TestSetup is Test { + PredictiveDeployer public implementation; + ERC1967Proxy public proxy; + ChildA public childA; + ChildB public childB; + bytes public bytecodeChildA; + bytes public bytecodeChildB; + bytes32 public hashedBytecodeChildA; + bytes32 public hashedBytecodeChildB; + + // Events + event Deploy(address indexed sender, address indexed childA, bytes32 hashedBytecode, uint256 nonce); + + function setUp() public { + implementation = new PredictiveDeployer(); + vm.prank(CONTRACT_DEPLOYER); + proxy = new ERC1967Proxy(address(implementation), abi.encodeWithSignature("initialize()")); + childA = new ChildA(address(this)); + childB = new ChildB(); + + // Get Bytecode + bytecodeChildA = abi.encodePacked(type(ChildA).creationCode, abi.encode(address(this))); + hashedBytecodeChildA = keccak256(bytecodeChildA); + bytecodeChildB = abi.encodePacked(type(ChildB).creationCode, abi.encode(address(this))); + hashedBytecodeChildB = keccak256(bytecodeChildB); + } +} diff --git a/test/helpers/DeploymentHelper.t.sol b/test/helpers/DeploymentHelper.t.sol new file mode 100644 index 0000000..c08b73d --- /dev/null +++ b/test/helpers/DeploymentHelper.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; +import { IPredictiveDeployer } from "../../src/interfaces/IPredictiveDeployer.sol"; + +abstract contract DeploymentHelper is Test { + /** + * @dev Reusable function for deploying childA contracts. + * @param _contract The address of the PredictveDeployer proxy contract. + * @param _wallet The principal's evm account that signs off on a childA contract deployment. + * @param _bytecode The to-be-deployed contract bytecode. + */ + function deployChild(address _contract, VmSafe.Wallet memory _wallet, bytes memory _bytecode) + internal + returns (address) + { + uint256 currentNonce = IPredictiveDeployer(_contract).userNonces(_wallet.addr, keccak256(_bytecode)); + bytes32 txHash = IPredictiveDeployer(_contract).getTransactionHash(_wallet.addr, _bytecode, currentNonce); + bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_wallet.privateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + return IPredictiveDeployer(_contract).deploy(_wallet.addr, signature, _bytecode); + } +} diff --git a/test/test_proxy/ERC1967Proxy.t.sol b/test/test_proxy/ERC1967Proxy.t.sol index e7bae1d..61bd995 100644 --- a/test/test_proxy/ERC1967Proxy.t.sol +++ b/test/test_proxy/ERC1967Proxy.t.sol @@ -1,25 +1,15 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.21; -import { Test } from "forge-std/Test.sol"; import { PredictiveDeployer } from "../../src/PredictiveDeployer.sol"; import { ERC1967Proxy } from "../../src/dependencies/proxy/ERC1967Proxy.sol"; import { IPredictiveDeployerAdmin } from "../../src/interfaces/IPredictiveDeployerAdmin.sol"; -import { CONTRACT_DEPLOYER } from "../common/constants.t.sol"; +import { TestSetup } from "../common/TestSetup.t.sol"; +import { CONTRACT_DEPLOYER } from "../common/Constants.t.sol"; -contract ERC1967ProxyTest is Test { +contract ERC1967ProxyTest is TestSetup { /* solhint-disable func-name-mixedcase */ - // Test Contracts - PredictiveDeployer public implementation; - ERC1967Proxy public proxy; - - function setUp() public { - implementation = new PredictiveDeployer(); - vm.prank(CONTRACT_DEPLOYER); - proxy = new ERC1967Proxy(address(implementation), abi.encodeWithSignature("initialize()")); - } - function test_upgradeToAndCall() public { // Setup bytes32 implentationStorageSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; From 14c34d77e6771b001a57af2d674c95db230baff8 Mon Sep 17 00:00:00 2001 From: cucupac <46691282+cucupac@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:59:00 -0500 Subject: [PATCH 3/3] test: add deploymentHistory test --- src/PredictiveDeployer.sol | 17 +++++-- src/interfaces/IPredictiveDeployer.sol | 1 + test/PredictiveDeployer.t.sol | 45 +++++++++++++++++-- .../{constants.t.sol => Constants.t.sol} | 0 test/helpers/libraries/AddressLib.t.sol | 13 ++++++ 5 files changed, 69 insertions(+), 7 deletions(-) rename test/common/{constants.t.sol => Constants.t.sol} (100%) create mode 100644 test/helpers/libraries/AddressLib.t.sol diff --git a/src/PredictiveDeployer.sol b/src/PredictiveDeployer.sol index 98a3d35..ba89a95 100644 --- a/src/PredictiveDeployer.sol +++ b/src/PredictiveDeployer.sol @@ -17,7 +17,7 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { // Factory Storage mapping(address => mapping(bytes32 => uint256)) public userNonces; - mapping(address => address[]) public deploymentHistory; + mapping(address => address[]) internal _deploymentHistory; // Events event Deploy(address indexed principal, address indexed child, bytes32 hashedBytecode, uint256 nonce); @@ -98,6 +98,15 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { return keccak256(_bytecode); } + /** + * @dev Returns a list of the provided principal's previously deployed child contracts. + * @param _principal The address of the account that signed the message hash. + * @return deploymentHistory A list of the provided principal's previously deployed child contracts. + */ + function getDeploymentHistory(address _principal) public view returns (address[] memory) { + return _deploymentHistory[_principal]; + } + /** * @dev Deploys arbitrary child contracts at predictable addresses, derived from account signatures. * @param _principal The address of the account that signed the message hash. @@ -130,12 +139,14 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { } // Update deployment history - deploymentHistory[_principal].push(child); + _deploymentHistory[_principal].push(child); emit Deploy(_principal, child, hashedByteCode, currentNonce); return child; } + receive() external payable { } // solhint-disable-line no-empty-blocks + /* **************************************************************************** ** ** Admin Functions @@ -158,6 +169,4 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable { SafeTransferLib.safeTransfer(ERC20(_token), msg.sender, balance); } - - receive() external payable { } // solhint-disable-line no-empty-blocks } diff --git a/src/interfaces/IPredictiveDeployer.sol b/src/interfaces/IPredictiveDeployer.sol index c192e2b..cf3820f 100644 --- a/src/interfaces/IPredictiveDeployer.sol +++ b/src/interfaces/IPredictiveDeployer.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.21; interface IPredictiveDeployer { function userNonces(address _principal, bytes32 _hashedBytecode) external returns (uint256); + function getDeploymentHistory(address _principal) external returns (address[] memory); function getTransactionHash(address _principal, bytes memory _bytecode, uint256 _nonce) external returns (bytes32); diff --git a/test/PredictiveDeployer.t.sol b/test/PredictiveDeployer.t.sol index 80e9c83..0b7154c 100644 --- a/test/PredictiveDeployer.t.sol +++ b/test/PredictiveDeployer.t.sol @@ -4,14 +4,17 @@ pragma solidity ^0.8.21; import { VmSafe } from "forge-std/Vm.sol"; import { PredictiveDeployer } from "../src/PredictiveDeployer.sol"; import { ERC1967Proxy } from "../src/dependencies/proxy/ERC1967Proxy.sol"; +import { IPredictiveDeployer } from "../src/interfaces/IPredictiveDeployer.sol"; import { TestSetup } from "./common/TestSetup.t.sol"; +import { AddressLib } from "./helpers/libraries/AddressLib.t.sol"; import { DeploymentHelper } from "./helpers/DeploymentHelper.t.sol"; -import { IPredictiveDeployer } from "../src/interfaces/IPredictiveDeployer.sol"; import { CONTRACT_DEPLOYER } from "./common/Constants.t.sol"; contract PredictiveDeployerTest is DeploymentHelper, TestSetup { /* solhint-disable func-name-mixedcase */ + using AddressLib for address[]; + function testFuzz_GetAddress(uint256 pkNum, address sender) public { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); @@ -67,12 +70,10 @@ contract PredictiveDeployerTest is DeploymentHelper, TestSetup { // Act address actualChild = IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA); - uint256 postDeployNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA); vm.stopPrank(); // Assertions assertEq(actualChild, expectedChild); - assertEq(postDeployNonce, preDeployNonce + 1); } function testFuzz_DeployTwice(uint256 pkNum) public { @@ -111,6 +112,44 @@ contract PredictiveDeployerTest is DeploymentHelper, TestSetup { assertEq(setOneChildB, setTwoChildB); } + function testFuzz_DeployNonceUpdate(uint256 pkNum) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); + uint256 preDeployNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA); + + // Act + deployChild(address(proxy), wallet, bytecodeChildA); + uint256 postDeployNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA); + + // Assertions + assertEq(postDeployNonce, preDeployNonce + 1); + } + + function testFuzz_DeployHistoryUpdate(uint256 pkNum) public { + // Setup + VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); + + address[] memory deploymentHistory = IPredictiveDeployer(address(proxy)).getDeploymentHistory(wallet.addr); + + // Pre-act assertions + assertEq(deploymentHistory.length, 0); + + // Act + address child1 = deployChild(address(proxy), wallet, bytecodeChildA); + deploymentHistory = IPredictiveDeployer(address(proxy)).getDeploymentHistory(wallet.addr); + + // Assertions + assertEq(deploymentHistory.length, 1); + assertTrue(deploymentHistory.includes(child1)); + + address child2 = deployChild(address(proxy), wallet, bytecodeChildA); + deploymentHistory = IPredictiveDeployer(address(proxy)).getDeploymentHistory(wallet.addr); + + // Assertions + assertEq(deploymentHistory.length, 2); + assertTrue(deploymentHistory.includes(child2)); + } + function testFuzz_CannotDeployReplay(uint256 pkNum) public { // Setup VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum))))); diff --git a/test/common/constants.t.sol b/test/common/Constants.t.sol similarity index 100% rename from test/common/constants.t.sol rename to test/common/Constants.t.sol diff --git a/test/helpers/libraries/AddressLib.t.sol b/test/helpers/libraries/AddressLib.t.sol new file mode 100644 index 0000000..f420790 --- /dev/null +++ b/test/helpers/libraries/AddressLib.t.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +library AddressLib { + function includes(address[] memory _array, address _address) internal pure returns (bool) { + for (uint256 i; i < _array.length; i++) { + if (_array[i] == _address) { + return true; + } + } + return false; + } +}