From 64014e49fc4784f7ce46b2b1a2fb29f3e2ac2a37 Mon Sep 17 00:00:00 2001 From: John Grant Date: Wed, 13 Sep 2023 20:09:32 +0100 Subject: [PATCH] feat(world): add world factory (#1385) Co-authored-by: alvrs --- .../world/abi/Create2.sol/Create2.abi.json | 1 + .../Create2Factory.abi.json | 39 +++++++++ .../Create2Factory.abi.json.d.ts | 40 +++++++++ .../IWorldFactory.sol/IWorldFactory.abi.json | 35 ++++++++ .../IWorldFactory.abi.json.d.ts | 36 ++++++++ .../WorldFactory.sol/WorldFactory.abi.json | 59 +++++++++++++ .../WorldFactory.abi.json.d.ts | 60 +++++++++++++ packages/world/src/factories/Create2.sol | 25 ++++++ .../world/src/factories/Create2Factory.sol | 19 +++++ .../world/src/factories/IWorldFactory.sol | 10 +++ packages/world/src/factories/WorldFactory.sol | 34 ++++++++ packages/world/test/Factories.t.sol | 84 +++++++++++++++++++ 12 files changed, 442 insertions(+) create mode 100644 packages/world/abi/Create2.sol/Create2.abi.json create mode 100644 packages/world/abi/Create2Factory.sol/Create2Factory.abi.json create mode 100644 packages/world/abi/Create2Factory.sol/Create2Factory.abi.json.d.ts create mode 100644 packages/world/abi/IWorldFactory.sol/IWorldFactory.abi.json create mode 100644 packages/world/abi/IWorldFactory.sol/IWorldFactory.abi.json.d.ts create mode 100644 packages/world/abi/WorldFactory.sol/WorldFactory.abi.json create mode 100644 packages/world/abi/WorldFactory.sol/WorldFactory.abi.json.d.ts create mode 100644 packages/world/src/factories/Create2.sol create mode 100644 packages/world/src/factories/Create2Factory.sol create mode 100644 packages/world/src/factories/IWorldFactory.sol create mode 100644 packages/world/src/factories/WorldFactory.sol create mode 100644 packages/world/test/Factories.t.sol diff --git a/packages/world/abi/Create2.sol/Create2.abi.json b/packages/world/abi/Create2.sol/Create2.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/Create2.sol/Create2.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/Create2Factory.sol/Create2Factory.abi.json b/packages/world/abi/Create2Factory.sol/Create2Factory.abi.json new file mode 100644 index 0000000000..cee9f1b1c0 --- /dev/null +++ b/packages/world/abi/Create2Factory.sol/Create2Factory.abi.json @@ -0,0 +1,39 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "addr", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "salt", + "type": "uint256" + } + ], + "name": "ContractDeployed", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "byteCode", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + } + ], + "name": "deployContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/world/abi/Create2Factory.sol/Create2Factory.abi.json.d.ts b/packages/world/abi/Create2Factory.sol/Create2Factory.abi.json.d.ts new file mode 100644 index 0000000000..317eae0a44 --- /dev/null +++ b/packages/world/abi/Create2Factory.sol/Create2Factory.abi.json.d.ts @@ -0,0 +1,40 @@ +declare const abi: [ + { + anonymous: false; + inputs: [ + { + indexed: false; + internalType: "address"; + name: "addr"; + type: "address"; + }, + { + indexed: false; + internalType: "uint256"; + name: "salt"; + type: "uint256"; + } + ]; + name: "ContractDeployed"; + type: "event"; + }, + { + inputs: [ + { + internalType: "bytes"; + name: "byteCode"; + type: "bytes"; + }, + { + internalType: "uint256"; + name: "salt"; + type: "uint256"; + } + ]; + name: "deployContract"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + } +]; +export default abi; diff --git a/packages/world/abi/IWorldFactory.sol/IWorldFactory.abi.json b/packages/world/abi/IWorldFactory.sol/IWorldFactory.abi.json new file mode 100644 index 0000000000..576d8b9ed4 --- /dev/null +++ b/packages/world/abi/IWorldFactory.sol/IWorldFactory.abi.json @@ -0,0 +1,35 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newContract", + "type": "address" + } + ], + "name": "WorldDeployed", + "type": "event" + }, + { + "inputs": [], + "name": "deployWorld", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "worldCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/world/abi/IWorldFactory.sol/IWorldFactory.abi.json.d.ts b/packages/world/abi/IWorldFactory.sol/IWorldFactory.abi.json.d.ts new file mode 100644 index 0000000000..a0c0cc06da --- /dev/null +++ b/packages/world/abi/IWorldFactory.sol/IWorldFactory.abi.json.d.ts @@ -0,0 +1,36 @@ +declare const abi: [ + { + anonymous: false; + inputs: [ + { + indexed: true; + internalType: "address"; + name: "newContract"; + type: "address"; + } + ]; + name: "WorldDeployed"; + type: "event"; + }, + { + inputs: []; + name: "deployWorld"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: []; + name: "worldCount"; + outputs: [ + { + internalType: "uint256"; + name: ""; + type: "uint256"; + } + ]; + stateMutability: "view"; + type: "function"; + } +]; +export default abi; diff --git a/packages/world/abi/WorldFactory.sol/WorldFactory.abi.json b/packages/world/abi/WorldFactory.sol/WorldFactory.abi.json new file mode 100644 index 0000000000..ea6d7bf3b2 --- /dev/null +++ b/packages/world/abi/WorldFactory.sol/WorldFactory.abi.json @@ -0,0 +1,59 @@ +[ + { + "inputs": [ + { + "internalType": "contract IModule", + "name": "_coreModule", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newContract", + "type": "address" + } + ], + "name": "WorldDeployed", + "type": "event" + }, + { + "inputs": [], + "name": "coreModule", + "outputs": [ + { + "internalType": "contract IModule", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deployWorld", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "worldCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/world/abi/WorldFactory.sol/WorldFactory.abi.json.d.ts b/packages/world/abi/WorldFactory.sol/WorldFactory.abi.json.d.ts new file mode 100644 index 0000000000..840546a35e --- /dev/null +++ b/packages/world/abi/WorldFactory.sol/WorldFactory.abi.json.d.ts @@ -0,0 +1,60 @@ +declare const abi: [ + { + inputs: [ + { + internalType: "contract IModule"; + name: "_coreModule"; + type: "address"; + } + ]; + stateMutability: "nonpayable"; + type: "constructor"; + }, + { + anonymous: false; + inputs: [ + { + indexed: true; + internalType: "address"; + name: "newContract"; + type: "address"; + } + ]; + name: "WorldDeployed"; + type: "event"; + }, + { + inputs: []; + name: "coreModule"; + outputs: [ + { + internalType: "contract IModule"; + name: ""; + type: "address"; + } + ]; + stateMutability: "view"; + type: "function"; + }, + { + inputs: []; + name: "deployWorld"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: []; + name: "worldCount"; + outputs: [ + { + internalType: "uint256"; + name: ""; + type: "uint256"; + } + ]; + stateMutability: "view"; + type: "function"; + } +]; +export default abi; diff --git a/packages/world/src/factories/Create2.sol b/packages/world/src/factories/Create2.sol new file mode 100644 index 0000000000..4b8dbbc6bf --- /dev/null +++ b/packages/world/src/factories/Create2.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +library Create2 { + /** + * @dev Deploys a contract using `CREATE2`. The address where the contract + * will be deployed can be known in advance. + * + * The bytecode for a contract can be obtained from Solidity with + * `type(contractName).creationCode`. + * + * Requirements: + * + * - `bytecode` must not be empty. + * - `salt` must have not been used for `bytecode` already. + */ + function deploy(bytes memory byteCode, uint256 salt) internal returns (address addr) { + assembly { + addr := create2(0, add(byteCode, 0x20), mload(byteCode), salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + } +} diff --git a/packages/world/src/factories/Create2Factory.sol b/packages/world/src/factories/Create2Factory.sol new file mode 100644 index 0000000000..a763ec5418 --- /dev/null +++ b/packages/world/src/factories/Create2Factory.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Create2 } from "./Create2.sol"; + +/** + @dev Helper Contract to facilitate create2 deployment of Contracts. +*/ +contract Create2Factory { + event ContractDeployed(address addr, uint256 salt); + + /** + * @dev Deploys a new Contract using create2. + */ + function deployContract(bytes memory byteCode, uint256 salt) public { + address addr = Create2.deploy(byteCode, salt); + emit ContractDeployed(addr, salt); + } +} diff --git a/packages/world/src/factories/IWorldFactory.sol b/packages/world/src/factories/IWorldFactory.sol new file mode 100644 index 0000000000..c76e646153 --- /dev/null +++ b/packages/world/src/factories/IWorldFactory.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface IWorldFactory { + event WorldDeployed(address indexed newContract); + + function worldCount() external view returns (uint256); + + function deployWorld() external; +} diff --git a/packages/world/src/factories/WorldFactory.sol b/packages/world/src/factories/WorldFactory.sol new file mode 100644 index 0000000000..318d225cf5 --- /dev/null +++ b/packages/world/src/factories/WorldFactory.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Create2 } from "./Create2.sol"; +import { World } from "../World.sol"; +import { IWorldFactory } from "./IWorldFactory.sol"; +import { IBaseWorld } from "../interfaces/IBaseWorld.sol"; +import { IModule } from "../interfaces/IModule.sol"; +import { ROOT_NAMESPACE } from "../constants.sol"; + +contract WorldFactory is IWorldFactory { + IModule public coreModule; + uint256 public worldCount; + + constructor(IModule _coreModule) { + coreModule = _coreModule; + } + + /** + @dev Deploy a new World, install the CoreModule and transfer ownership to the caller + */ + function deployWorld() public { + // Deploy a new World and increase the WorldCount + bytes memory bytecode = type(World).creationCode; + address worldAddress = Create2.deploy(bytecode, worldCount++); + IBaseWorld world = IBaseWorld(worldAddress); + + // Initialize the World and transfer ownership to the caller + world.installRootModule(coreModule, new bytes(0)); + world.transferOwnership(ROOT_NAMESPACE, msg.sender); + + emit WorldDeployed(worldAddress); + } +} diff --git a/packages/world/test/Factories.t.sol b/packages/world/test/Factories.t.sol new file mode 100644 index 0000000000..0609c16938 --- /dev/null +++ b/packages/world/test/Factories.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Test, console } from "forge-std/Test.sol"; + +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { World } from "../src/World.sol"; +import { CoreModule } from "../src/modules/core/CoreModule.sol"; +import { Create2Factory } from "../src/factories/Create2Factory.sol"; +import { WorldFactory } from "../src/factories/WorldFactory.sol"; +import { IWorldFactory } from "../src/factories/IWorldFactory.sol"; +import { InstalledModules, InstalledModulesData } from "../src/tables/InstalledModules.sol"; +import { NamespaceOwner } from "../src/tables/NamespaceOwner.sol"; +import { ROOT_NAMESPACE } from "../src/constants.sol"; + +contract FactoriesTest is Test { + event ContractDeployed(address addr, uint256 salt); + event WorldDeployed(address indexed newContract); + event HelloWorld(); + + function calculateAddress( + address deployingAddress, + bytes32 salt, + bytes memory bytecode + ) internal pure returns (address) { + bytes32 bytecodeHash = keccak256(bytecode); + bytes32 data = keccak256(abi.encodePacked(bytes1(0xff), deployingAddress, salt, bytecodeHash)); + return address(uint160(uint256(data))); + } + + function testCreate2Factory() public { + Create2Factory create2Factory = new Create2Factory(); + + // Encode constructor arguments for WorldFactory + bytes memory encodedArguments = abi.encode(new CoreModule()); + bytes memory combinedBytes = abi.encodePacked(type(WorldFactory).creationCode, encodedArguments); + + // Address we expect for deployed WorldFactory + address calculatedAddress = calculateAddress(address(create2Factory), bytes32(0), combinedBytes); + + // Confirm event for deployment + vm.expectEmit(true, false, false, false); + emit ContractDeployed(calculatedAddress, uint256(0)); + create2Factory.deployContract(combinedBytes, uint256(0)); + + // Confirm worldFactory was deployed correctly + IWorldFactory worldFactory = IWorldFactory(calculatedAddress); + assertEq(uint256(worldFactory.worldCount()), uint256(0)); + } + + function testWorldFactory() public { + // Deploy WorldFactory with current CoreModule + CoreModule coreModule = new CoreModule(); + address worldFactoryAddress = address(new WorldFactory(coreModule)); + IWorldFactory worldFactory = IWorldFactory(worldFactoryAddress); + + // Address we expect for World + address calculatedAddress = calculateAddress(worldFactoryAddress, bytes32(0), type(World).creationCode); + + // Check for HelloWorld event from World + vm.expectEmit(true, false, false, false); + emit HelloWorld(); + + // Check for WorldDeployed event from Factory + vm.expectEmit(true, false, false, false); + emit WorldDeployed(calculatedAddress); + worldFactory.deployWorld(); + + // Set the store address manually + StoreSwitch.setStoreAddress(calculatedAddress); + + // Retrieve CoreModule address from InstalledModule table + InstalledModulesData memory installedModule = InstalledModules.get(bytes16("core.m"), keccak256(new bytes(0))); + + // Confirm correct Core is installed + assertEq(installedModule.moduleAddress, address(coreModule)); + + // Confirm worldCount (which is salt) has incremented + assertEq(uint256(worldFactory.worldCount()), uint256(1)); + + // Confirm the msg.sender is owner of the root namespace of the new world + assertEq(NamespaceOwner.get(ROOT_NAMESPACE), address(this)); + } +}