-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Step 1 of 2] feat: add create2Factory (#1)
* 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
Showing
19 changed files
with
356 additions
and
58 deletions.
There are no files selected for viewing
1 change: 1 addition & 0 deletions
1
.forge-snapshots/Create2FactoryTest#test_Deploy_ContractWithArgs.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
106498 |
1 change: 1 addition & 0 deletions
1
.forge-snapshots/Create2FactoryTest#test_SetWhitelistedUser_false.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
25860 |
1 change: 1 addition & 0 deletions
1
.forge-snapshots/Create2FactoryTest#test_SetWhitelistedUser_true.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
47772 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Submodule forge-gas-snapshot
added at
cf34ad
Submodule openzeppelin-contracts
added at
dbb610
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.