Skip to content

Commit

Permalink
refactor: improve test coverage across all constructor arg types, ref…
Browse files Browse the repository at this point in the history
…actory create2 tests
  • Loading branch information
cucupac committed Nov 7, 2023
1 parent 46bb0d5 commit f7d4638
Show file tree
Hide file tree
Showing 24 changed files with 549 additions and 264 deletions.
39 changes: 19 additions & 20 deletions src/create3/Create3Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,48 +57,47 @@ contract Create3Factory is Initializable, UUPSUpgradeable, Ownable {
/**
* @dev Computes the expected message hash.
* @param _principal The address of the account for whom this factory contract will deploy a child contract.
* @param _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @param _creationCode The bytecode of the contract to be deployed without the constructor arguments.
* @return messageHash The expected signed message hash.
*/
function _computeMessageHash(address _principal, bytes memory _strippedBytecode) internal view returns (bytes32) {
bytes32 txHash = getTransactionHash(_principal, _strippedBytecode);
function _computeMessageHash(address _principal, bytes memory _creationCode) internal view returns (bytes32) {
bytes32 txHash = getTransactionHash(_principal, _creationCode);
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));
}

/**
* @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 _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @param _creationCode The bytecode of the contract to be deployed without the constructor arguments.
* @return txHash The unique deployment transaction hash to be signed.
*/
function getTransactionHash(address _principal, bytes memory _strippedBytecode) public view returns (bytes32) {
function getTransactionHash(address _principal, bytes memory _creationCode) public view returns (bytes32) {
return keccak256(
abi.encodePacked(
domainSeparator, _principal, _strippedBytecode, userNonces[_principal][keccak256(_strippedBytecode)]
domainSeparator, _principal, _creationCode, userNonces[_principal][keccak256(_creationCode)]
)
);
}

/**
* @dev Returns the predicted address of a contract to be deployed before it's deployed.
* @param _principal The address of the account that signed the message hash.
* @param _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @param _creationCode The bytecode of the contract to be deployed without the constructor arguments.
* @return child The predicted address of the contract to be deployed.
*/
function getAddress(address _principal, bytes memory _strippedBytecode) public view returns (address) {
bytes32 salt = keccak256(
abi.encodePacked(_principal, _strippedBytecode, userNonces[_principal][keccak256(_strippedBytecode)])
);
function getAddress(address _principal, bytes memory _creationCode) public view returns (address) {
bytes32 salt =
keccak256(abi.encodePacked(_principal, _creationCode, userNonces[_principal][keccak256(_creationCode)]));
return CREATE3.getDeployed(salt);
}

/**
* @dev Returns the keccak256 hash of the provided stripped bytecode.
* @param _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @param _creationCode The bytecode of the contract to be deployed without the constructor arguments.
* @return hash The keccak256 hash of the provided stripped bytecode.
*/
function getBytecodeHash(bytes memory _strippedBytecode) public pure returns (bytes32) {
return keccak256(_strippedBytecode);
function getBytecodeHash(bytes memory _creationCode) public pure returns (bytes32) {
return keccak256(_creationCode);
}

/**
Expand All @@ -114,18 +113,18 @@ contract Create3Factory is Initializable, UUPSUpgradeable, Ownable {
* @dev Deploys arbitrary child contracts at predictable addresses, derived from account signatures.
* @param _principal The address of the account that signed the message hash.
* @param _signature The resulting signature from the principal account signing the messahge hash.
* @param _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @param _creationCode The bytecode of the contract to be deployed without the constructor arguments.
* @param _constructorArgsBytecode The encoded constructor arguments of the contract to be deployed.
*/
function deploy(
address _principal,
bytes memory _signature,
bytes memory _strippedBytecode,
bytes memory _creationCode,
bytes memory _constructorArgsBytecode
) public payable {
bytes32 hashedStrippedBytecode = keccak256(_strippedBytecode);
bytes32 hashedStrippedBytecode = keccak256(_creationCode);
uint256 currentNonce = userNonces[_principal][hashedStrippedBytecode];
bytes32 expectedMessageHash = _computeMessageHash(_principal, _strippedBytecode);
bytes32 expectedMessageHash = _computeMessageHash(_principal, _creationCode);

// Ensure the provided principal signed the expected message hash
if (ECDSA.recover(expectedMessageHash, _signature) != _principal) revert Unauthorized();
Expand All @@ -134,10 +133,10 @@ contract Create3Factory is Initializable, UUPSUpgradeable, Ownable {
userNonces[_principal][hashedStrippedBytecode]++;

// Calculate salt
bytes32 salt = keccak256(abi.encodePacked(_principal, _strippedBytecode, currentNonce));
bytes32 salt = keccak256(abi.encodePacked(_principal, _creationCode, currentNonce));

// Deploy
address child = CREATE3.deploy(salt, abi.encodePacked(_strippedBytecode, _constructorArgsBytecode), msg.value);
address child = CREATE3.deploy(salt, abi.encodePacked(_creationCode, _constructorArgsBytecode), msg.value);

// Update deployment history
_deploymentHistory[_principal].push(child);
Expand Down
8 changes: 4 additions & 4 deletions src/create3/interfaces/ICreate3Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ pragma solidity ^0.8.21;

interface ICreate3Factory {
function userNonces(address _principal, bytes32 hashedStrippedBytecode) external returns (uint256);
function getTransactionHash(address _principal, bytes memory _strippedBytecode) external returns (bytes32);
function getAddress(address _principal, bytes memory _strippedBytecode) external returns (address);
function getBytecodeHash(bytes memory _strippedBytecode) external pure returns (bytes32);
function getTransactionHash(address _principal, bytes memory _creationCode) external returns (bytes32);
function getAddress(address _principal, bytes memory _creationCode) external returns (address);
function getBytecodeHash(bytes memory _creationCode) external pure returns (bytes32);
function getDeploymentHistory(address _principal) external returns (address[] memory);
function deploy(
address _principal,
bytes memory _signature,
bytes memory _strippedBytecode,
bytes memory _creationCode,
bytes memory _constructorArgsBytecode
) external payable;
}
24 changes: 0 additions & 24 deletions test/common/Child.t.sol

This file was deleted.

4 changes: 2 additions & 2 deletions test/create2/Create2Factory.admin.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { Create2Factory } from "../../src/create2/Create2Factory.sol";
import { ERC1967Proxy } from "../../src/dependencies/proxy/ERC1967Proxy.sol";
import { IERC20 } from "../../src/dependencies/token/interfaces/IERC20.sol";
import { ICreate2FactoryAdmin } from "../../src/create2/interfaces/ICreate2FactoryAdmin.sol";
import { TestSetup } from "./common/TestSetup.t.sol";
import { CONTRACT_DEPLOYER, TEST_ERC20_TOKEN } from "../common/Constants.t.sol";
import { TestSetup } from "./common/contracts/TestSetup.t.sol";
import { CONTRACT_DEPLOYER, TEST_ERC20_TOKEN } from "./common/Constants.t.sol";

contract Create2FactoryTest is TestSetup {
/* solhint-disable func-name-mixedcase */
Expand Down
64 changes: 34 additions & 30 deletions test/create2/Create2Factory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { VmSafe } from "forge-std/Vm.sol";
import { Create2Factory } from "../../src/create2/Create2Factory.sol";
import { ERC1967Proxy } from "../../src/dependencies/proxy/ERC1967Proxy.sol";
import { ICreate2Factory } from "../../src/create2/interfaces/ICreate2Factory.sol";
import { TestSetup } from "./common/TestSetup.t.sol";
import { AddressLib } from "../common/libraries/AddressLib.t.sol";
import { TestSetup } from "./common/contracts/TestSetup.t.sol";
import { AddressLib } from "./common/libraries/AddressLib.t.sol";
import { DeploymentHelper } from "./helpers/DeploymentHelper.t.sol";
import { CONTRACT_DEPLOYER } from "../common/Constants.t.sol";
import { CONTRACT_DEPLOYER } from "./common/Constants.t.sol";

contract Create2FactoryTest is DeploymentHelper, TestSetup {
/* solhint-disable func-name-mixedcase */
Expand All @@ -19,10 +19,11 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

uint256 currentNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA);
uint256 currentNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildWithArgs);

// Get signature information
bytes32 txHash = ICreate2Factory(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildA, currentNonce);
bytes32 txHash =
ICreate2Factory(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildWithArgs, currentNonce);

bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));

Expand All @@ -33,13 +34,13 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {
// Expectation
vm.startPrank(sender);
uint256 snapShot = vm.snapshot();
address expectedChild = ICreate2Factory(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA);
address expectedChild = ICreate2Factory(address(proxy)).deploy(wallet.addr, signature, bytecodeChildWithArgs);

// Set chain state to what it was before the deployment
vm.revertTo(snapShot);

// Act
address actualChild = ICreate2Factory(address(proxy)).getAddress(wallet.addr, bytecodeChildA);
address actualChild = ICreate2Factory(address(proxy)).getAddress(wallet.addr, bytecodeChildWithArgs);
vm.stopPrank();

// Assertions
Expand All @@ -50,10 +51,11 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

uint256 preDeployNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA);
uint256 preDeployNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildWithArgs);

// Get signature information
bytes32 txHash = ICreate2Factory(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildA, preDeployNonce);
bytes32 txHash =
ICreate2Factory(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildWithArgs, preDeployNonce);

bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));

Expand All @@ -62,12 +64,12 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {

// Expectations
vm.startPrank(sender);
address expectedChild = ICreate2Factory(address(proxy)).getAddress(wallet.addr, bytecodeChildA);
address expectedChild = ICreate2Factory(address(proxy)).getAddress(wallet.addr, bytecodeChildWithArgs);
vm.expectEmit(true, true, true, true, address(proxy));
emit Deploy(wallet.addr, expectedChild, keccak256(bytecodeChildA), preDeployNonce);
emit Deploy(wallet.addr, expectedChild, keccak256(bytecodeChildWithArgs), preDeployNonce);

// Act
address actualChild = ICreate2Factory(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA);
address actualChild = ICreate2Factory(address(proxy)).deploy(wallet.addr, signature, bytecodeChildWithArgs);
vm.stopPrank();

// Assertions
Expand All @@ -79,10 +81,10 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

// First Deployment
address actualChild1 = deployChild(address(proxy), wallet, bytecodeChildA);
address actualChild1 = deployChild(address(proxy), wallet, bytecodeChildWithArgs);

// Second Deployment
address actualChild2 = deployChild(address(proxy), wallet, bytecodeChildA);
address actualChild2 = deployChild(address(proxy), wallet, bytecodeChildWithArgs);

// Assertions
assertTrue(actualChild1 != address(0) && actualChild2 != address(0));
Expand All @@ -95,15 +97,15 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {

// First deployment set
uint256 snapShot = vm.snapshot();
address setOneChildA = deployChild(address(proxy), wallet, bytecodeChildA);
address setOneChildB = deployChild(address(proxy), wallet, bytecodeChildB);
address setOneChildA = deployChild(address(proxy), wallet, bytecodeChildWithArgs);
address setOneChildB = deployChild(address(proxy), wallet, bytecodeChildNoArgs);

// 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);
address setTwoChildB = deployChild(address(proxy), wallet, bytecodeChildNoArgs);
address setTwoChildA = deployChild(address(proxy), wallet, bytecodeChildWithArgs);

// Assertions
assertEq(setOneChildA, setTwoChildA);
Expand All @@ -113,11 +115,11 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {
function testFuzz_DeployNonceUpdate(uint256 pkNum) public {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));
uint256 preDeployNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA);
uint256 preDeployNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildWithArgs);

// Act
deployChild(address(proxy), wallet, bytecodeChildA);
uint256 postDeployNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA);
deployChild(address(proxy), wallet, bytecodeChildWithArgs);
uint256 postDeployNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildWithArgs);

// Assertions
assertEq(postDeployNonce, preDeployNonce + 1);
Expand All @@ -133,14 +135,14 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {
assertEq(deploymentHistory.length, 0);

// Act
address child1 = deployChild(address(proxy), wallet, bytecodeChildA);
address child1 = deployChild(address(proxy), wallet, bytecodeChildWithArgs);
deploymentHistory = ICreate2Factory(address(proxy)).getDeploymentHistory(wallet.addr);

// Assertions
assertEq(deploymentHistory.length, 1);
assertTrue(deploymentHistory.includes(child1));

address child2 = deployChild(address(proxy), wallet, bytecodeChildA);
address child2 = deployChild(address(proxy), wallet, bytecodeChildWithArgs);
deploymentHistory = ICreate2Factory(address(proxy)).getDeploymentHistory(wallet.addr);

// Assertions
Expand All @@ -152,22 +154,23 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

uint256 currentNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA);
uint256 currentNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildWithArgs);

// Get signature information
bytes32 txHash = ICreate2Factory(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildA, currentNonce);
bytes32 txHash =
ICreate2Factory(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildWithArgs, 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);

// Deploy once
ICreate2Factory(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA);
ICreate2Factory(address(proxy)).deploy(wallet.addr, signature, bytecodeChildWithArgs);

// Act: attempt replay
vm.expectRevert(Create2Factory.Unauthorized.selector);
ICreate2Factory(address(proxy)).deploy(wallet.addr, signature, bytecodeChildA);
ICreate2Factory(address(proxy)).deploy(wallet.addr, signature, bytecodeChildWithArgs);
}

function testFuzz_CannotDeployWithoutApproval(uint256 pkNum, address invalidPrincipal) public {
Expand All @@ -177,10 +180,11 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {
// Failing condition: principal is not the signer
vm.assume(invalidPrincipal != wallet.addr);

uint256 currentNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildA);
uint256 currentNonce = ICreate2Factory(address(proxy)).userNonces(wallet.addr, hashedBytecodeChildWithArgs);

// Get signature information
bytes32 txHash = ICreate2Factory(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildA, currentNonce);
bytes32 txHash =
ICreate2Factory(address(proxy)).getTransactionHash(wallet.addr, bytecodeChildWithArgs, currentNonce);

bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));

Expand All @@ -189,7 +193,7 @@ contract Create2FactoryTest is DeploymentHelper, TestSetup {

// Act: attempt with invalid principal
vm.expectRevert(Create2Factory.Unauthorized.selector);
ICreate2Factory(address(proxy)).deploy(invalidPrincipal, signature, bytecodeChildA);
ICreate2Factory(address(proxy)).deploy(invalidPrincipal, signature, bytecodeChildWithArgs);
}

function testFuzz_GetBytecodeHash(bytes memory _childBytecode) public {
Expand Down
File renamed without changes.
Loading

0 comments on commit f7d4638

Please sign in to comment.