Skip to content

Commit

Permalink
[Step 1 of 2] feat: add create2Factory (#1)
Browse files Browse the repository at this point in the history
* forge install: openzeppelin-contracts

v5.0.2

* feat: add create2 factory

* feat: clean up and make create2Factory more friendly

* test: add more test

* feat: include deployment script

* updated foundry_out

* forge install: forge-gas-snapshot

* feat: clean up unnecessary files and add gas snapshot

* feat: adjust higher optimizer runs for better gas

* feat: add interfce and clean up

* feedback: add nonReentrant for deploy and update test comment

* feat: add test on bsc/eth forks to verify

* include env variable in workflow to test
  • Loading branch information
ChefMist authored Oct 15, 2024
1 parent 33aa05a commit 9e648d1
Show file tree
Hide file tree
Showing 19 changed files with 356 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
106498
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
25860
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
47772
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ jobs:

- name: Run tests
run: forge test -vvv --isolate
env:
TESTNET_FORK_URL_BSC: ${{ secrets.TESTNET_FORK_URL_BSC }}
TESTNET_FORK_URL_SEPOLIA: ${{ secrets.TESTNET_FORK_URL_SEPOLIA }}
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/forge-gas-snapshot"]
path = lib/forge-gas-snapshot
url = https://github.com/marktoda/forge-gas-snapshot
9 changes: 8 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
[profile.default]
src = "src"
out = "out"
out = 'foundry-out'
libs = ["lib"]
ffi = true
fs_permissions = [
{ access = "read-write", path = ".forge-snapshots/" },
{ access = "read", path = "./foundry-out" },
]
evm_version = 'cancun'
optimizer_runs = 30_000

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions lib/forge-gas-snapshot
Submodule forge-gas-snapshot added at cf34ad
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at dbb610
4 changes: 4 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/
forge-gas-snapshot/=lib/forge-gas-snapshot/src/
@openzeppelin/=lib/openzeppelin-contracts/
28 changes: 28 additions & 0 deletions script/01_DeployCreate2Factory.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import "forge-std/src/Script.sol";
import {Create2Factory} from "../src/Create2Factory.sol";

/**
* forge script script/01_DeployCreate2Factory.s.sol:DeployCreate2FactoryScript -vvv \
* --rpc-url $RPC_URL \
* --broadcast \
* --slow \
* --verify
*/
contract DeployCreate2FactoryScript is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

// Sanity check to use nonce 0, to ensure the contract is deployed at the same address on other chain
uint64 nonce = vm.getNonce(vm.addr(deployerPrivateKey));
vm.assertEq(nonce, 0, "Must create contract with nonce 0");

Create2Factory create2Factory = new Create2Factory();
console.log("Create2Factory contract deployed at ", address(create2Factory));

vm.stopBroadcast();
}
}
19 changes: 0 additions & 19 deletions script/Counter.s.sol

This file was deleted.

14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

54 changes: 54 additions & 0 deletions src/Create2Factory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;

import {ICreate2Factory} from "./interfaces/ICreate2Factory.sol";
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/// @notice Deploy contracts in a deterministic way using CREATE2
/// @dev ensure this contract is deployed on multiple chain with the same address
contract Create2Factory is ICreate2Factory, Ownable2Step, ReentrancyGuard {
event SetWhitelist(address indexed user, bool isWhitelist);

// Only whitelisted user can interact with create2Factory
mapping(address user => bool isWhitelisted) public isUserWhitelisted;

modifier onlyWhitelisted() {
require(isUserWhitelisted[msg.sender], "Create2Factory: caller is not whitelisted");
_;
}

constructor() Ownable(msg.sender) {
isUserWhitelisted[msg.sender] = true;
}

/// @inheritdoc ICreate2Factory
function deploy(bytes32 salt, bytes memory creationCode)
external
payable
onlyWhitelisted
nonReentrant
returns (address deployed)
{
deployed = Create2.deploy(msg.value, salt, creationCode);
}

/// @inheritdoc ICreate2Factory
function computeAddress(bytes32 salt, bytes32 bytecodeHash) public view returns (address) {
return Create2.computeAddress(salt, bytecodeHash);
}

/// @inheritdoc ICreate2Factory
function execute(address target, bytes calldata data) external payable onlyWhitelisted nonReentrant {
(bool success,) = target.call{value: msg.value}(data);
require(success, "Create2Factory: failed execute call");
}

/// @inheritdoc ICreate2Factory
function setWhitelistUser(address user, bool isWhiteList) external onlyOwner {
isUserWhitelisted[user] = isWhiteList;

emit SetWhitelist(user, isWhiteList);
}
}
18 changes: 18 additions & 0 deletions src/interfaces/ICreate2Factory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ICreate2Factory {
/// @notice create2 deploy a contract
/// @dev So long the same salt, creationCode is used, the contract will be deployed at the same address on other chain
function deploy(bytes32 salt, bytes memory creationCode) external payable returns (address deployed);

/// @notice compute the create2 address based on salt, bytecodeHash
function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address);

/// @notice execute a call on a deployed contract
/// @dev used in scenario where contract owner is create2Factory and we need to transfer ownership
function execute(address target, bytes calldata data) external payable;

/// @notice set user as whitelisted
function setWhitelistUser(address user, bool isWhiteList) external;
}
24 changes: 0 additions & 24 deletions test/Counter.t.sol

This file was deleted.

148 changes: 148 additions & 0 deletions test/Create2Factory.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import "forge-std/src/Test.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol";
import {Create2Factory} from "../src/Create2Factory.sol";
import {MockOwner} from "./mocks/MockOwner.sol";
import {MockWithConstructorArgs} from "./mocks/MockWithConstructorArgs.sol";

contract Create2FactoryTest is Test, GasSnapshot {
Create2Factory create2Factory;

address pcsDeployer = makeAddr("pcsDeployer");
address alice = makeAddr("alice");

function setUp() public {
create2Factory = new Create2Factory();
create2Factory.setWhitelistUser(pcsDeployer, true);
}

function test_Deploy_ContractWithArgs() public {
// deploy
bytes memory creationCode = abi.encodePacked(type(MockWithConstructorArgs).creationCode, abi.encode(42));
bytes32 salt = bytes32(uint256(0x1234));
address deployed = create2Factory.deploy(salt, creationCode);
snapLastCall("Create2FactoryTest#test_Deploy_ContractWithArgs");

// verify
address expectedDeployed = create2Factory.computeAddress(salt, keccak256(creationCode));
assertEq(deployed, expectedDeployed);

MockWithConstructorArgs deployedContract = MockWithConstructorArgs(deployed);
assertEq(deployedContract.args(), 42);
}

/// @dev different nonce should still deploy at the same address
function test_Deploy_DifferentNonce(uint64 nonce) public {
vm.setNonce(pcsDeployer, nonce);
vm.startPrank(pcsDeployer);

// deploy
bytes memory creationCode = type(MockOwner).creationCode;
bytes32 salt = bytes32(uint256(0x1234));
address deployed = create2Factory.deploy(salt, creationCode);

// verify
address expectedDeployed = create2Factory.computeAddress(salt, keccak256(creationCode));
assertEq(deployed, expectedDeployed);
}

/// @dev deployment address should match with different address
function test_Deploy_DifferentDeployer(address deployer) public {
vm.assume(deployer != address(0));
create2Factory.setWhitelistUser(deployer, true);

// deploy as deployer
bytes memory creationCode = type(MockOwner).creationCode;
bytes32 salt = bytes32(uint256(0x1234));
vm.prank(deployer);
address deployed = create2Factory.deploy(salt, creationCode);

// verify
address expectedDeployed = create2Factory.computeAddress(salt, keccak256(creationCode));
assertEq(deployed, expectedDeployed);
}

function test_Deploy_NotWhitelisted() public {
vm.startPrank(alice);

// deploy
bytes memory creationCode = type(MockOwner).creationCode;
bytes32 salt = bytes32(uint256(0x1234));
vm.expectRevert("Create2Factory: caller is not whitelisted");
create2Factory.deploy(salt, creationCode);
}

function test_Execute() public {
vm.startPrank(pcsDeployer);

// deploy
bytes memory creationCode = type(MockOwner).creationCode;
bytes32 salt = bytes32(uint256(0x1234));
address deployed = create2Factory.deploy(salt, creationCode);

// verify
MockOwner owner = MockOwner(deployed);
assertEq(owner.owner(), address(create2Factory));

// execute
bytes memory data = abi.encodeWithSignature("transferOwnership(address)", pcsDeployer);
create2Factory.execute(deployed, data);
assertEq(owner.owner(), pcsDeployer);
}

function test_Execute_Payable() public {
vm.deal(pcsDeployer, 1 ether);
vm.startPrank(pcsDeployer);

// deploy
bytes memory creationCode = type(MockOwner).creationCode;
bytes32 salt = bytes32(uint256(0x1234));
address deployed = create2Factory.deploy(salt, creationCode);

// before
assertEq(deployed.balance, 0);

// execute
bytes memory data = abi.encodeWithSignature("payableFunc()");
create2Factory.execute{value: 1 ether}(deployed, data);

// after
assertEq(deployed.balance, 1 ether);
}

function test_Execute_NotWhitelisted() public {
vm.prank(alice);
vm.expectRevert("Create2Factory: caller is not whitelisted");
bytes memory data = abi.encodeWithSignature("transferOwnership(address)", alice);
create2Factory.execute(makeAddr("random"), data);
}

function test_SetWhitelistedUser() public {
// before
assertEq(create2Factory.isUserWhitelisted(alice), false);

// set whitelisted
vm.expectEmit();
emit Create2Factory.SetWhitelist(alice, true);
create2Factory.setWhitelistUser(alice, true);
snapLastCall("Create2FactoryTest#test_SetWhitelistedUser_true");
assertEq(create2Factory.isUserWhitelisted(alice), true);

// set not whitelisted
vm.expectEmit();
emit Create2Factory.SetWhitelist(alice, false);
create2Factory.setWhitelistUser(alice, false);
snapLastCall("Create2FactoryTest#test_SetWhitelistedUser_false");
assertEq(create2Factory.isUserWhitelisted(alice), false);
}

function test_SetWhitelistUser_OnlyOwner() public {
vm.prank(alice);

vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
create2Factory.setWhitelistUser(alice, true);
}
}
Loading

0 comments on commit 9e648d1

Please sign in to comment.