Skip to content

Commit

Permalink
feat(world): add WorldProxy and WorldProxyFactory (#2632)
Browse files Browse the repository at this point in the history
Co-authored-by: alvarius <[email protected]>
  • Loading branch information
yonadaaa and alvrs authored Apr 19, 2024
1 parent 534e772 commit 3d1d590
Show file tree
Hide file tree
Showing 19 changed files with 678 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changeset/spicy-bags-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@latticexyz/world": patch
"@latticexyz/cli": patch
---

Added a `deploy.useProxy` option to the MUD config that deploys the World as an upgradable proxy contract. The proxy behaves like a regular World contract, but the underlying implementation can be upgraded by calling `setImplementation`.
6 changes: 4 additions & 2 deletions packages/cli/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type DeployOptions<configInput extends ConfigInput> = {
* not have a deterministic address.
*/
deployerAddress?: Hex;
withWorldProxy?: boolean;
};

/**
Expand All @@ -42,12 +43,13 @@ export async function deploy<configInput extends ConfigInput>({
salt,
worldAddress: existingWorldAddress,
deployerAddress: initialDeployerAddress,
withWorldProxy,
}: DeployOptions<configInput>): Promise<WorldDeploy> {
const tables = Object.values(config.tables) as Table[];

const deployerAddress = initialDeployerAddress ?? (await ensureDeployer(client));

await ensureWorldFactory(client, deployerAddress);
await ensureWorldFactory(client, deployerAddress, withWorldProxy);

// deploy all dependent contracts, because system registration, module install, etc. all expect these contracts to be callable.
await ensureContractsDeployed({
Expand All @@ -74,7 +76,7 @@ export async function deploy<configInput extends ConfigInput>({

const worldDeploy = existingWorldAddress
? await getWorldDeploy(client, existingWorldAddress)
: await deployWorld(client, deployerAddress, salt ?? `0x${randomBytes(32).toString("hex")}`);
: await deployWorld(client, deployerAddress, salt ?? `0x${randomBytes(32).toString("hex")}`, withWorldProxy);

if (!supportedStoreVersions.includes(worldDeploy.storeVersion)) {
throw new Error(`Unsupported Store version: ${worldDeploy.storeVersion}`);
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/deploy/deployWorld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export async function deployWorld(
client: Client<Transport, Chain | undefined, Account>,
deployerAddress: Hex,
salt: Hex,
withWorldProxy?: boolean,
): Promise<WorldDeploy> {
const worldFactory = await ensureWorldFactory(client, deployerAddress);
const worldFactory = await ensureWorldFactory(client, deployerAddress, withWorldProxy);

debug("deploying world");
const tx = await writeContract(client, {
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/src/deploy/ensureWorldFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import initModuleBuild from "@latticexyz/world/out/InitModule.sol/InitModule.jso
import initModuleAbi from "@latticexyz/world/out/InitModule.sol/InitModule.abi.json" assert { type: "json" };
import worldFactoryBuild from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.json" assert { type: "json" };
import worldFactoryAbi from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.abi.json" assert { type: "json" };
import worldProxyFactoryBuild from "@latticexyz/world/out/WorldProxyFactory.sol/WorldProxyFactory.json" assert { type: "json" };
import worldProxyFactoryAbi from "@latticexyz/world/out/WorldProxyFactory.sol/WorldProxyFactory.abi.json" assert { type: "json" };
import { Client, Transport, Chain, Account, Hex, getCreate2Address, encodeDeployData, size, Address } from "viem";
import { salt } from "./common";
import { ensureContractsDeployed } from "./ensureContractsDeployed";
Expand All @@ -14,6 +16,7 @@ import { Contract } from "./ensureContract";
export async function ensureWorldFactory(
client: Client<Transport, Chain | undefined, Account>,
deployerAddress: Hex,
withWorldProxy?: boolean,
): Promise<Address> {
const accessManagementSystemDeployedBytecodeSize = size(accessManagementSystemBuild.deployedBytecode.object as Hex);
const accessManagementSystemBytecode = accessManagementSystemBuild.bytecode.object as Hex;
Expand Down Expand Up @@ -52,10 +55,13 @@ export async function ensureWorldFactory(

const initModule = getCreate2Address({ from: deployerAddress, bytecode: initModuleBytecode, salt });

const worldFactoryDeployedBytecodeSize = size(worldFactoryBuild.deployedBytecode.object as Hex);
const build = withWorldProxy ? worldProxyFactoryBuild : worldFactoryBuild;
const abi = withWorldProxy ? worldProxyFactoryAbi : worldFactoryAbi;

const worldFactoryDeployedBytecodeSize = size(build.deployedBytecode.object as Hex);
const worldFactoryBytecode = encodeDeployData({
bytecode: worldFactoryBuild.bytecode.object as Hex,
abi: worldFactoryAbi,
bytecode: build.bytecode.object as Hex,
abi: abi,
args: [initModule],
});

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/runDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ in your contracts directory to use the default anvil private key.`,
worldAddress: opts.worldAddress as Hex | undefined,
client,
config: resolvedConfig,
withWorldProxy: configV2.deploy.useProxy,
});
if (opts.worldAddress == null || opts.alwaysRunPostDeploy) {
await postDeploy(config.postDeployScript, worldDeploy.address, rpc, profile);
Expand Down
108 changes: 108 additions & 0 deletions packages/world/gas-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,114 @@
"name": "update in field 1 item (warm)",
"gasUsed": 44023
},
{
"file": "test/WorldProxy.t.sol",
"test": "testCall",
"name": "call a system via the World",
"gasUsed": 45004
},
{
"file": "test/WorldProxy.t.sol",
"test": "testCallFromNamespaceDelegation",
"name": "call a system via a namespace fallback delegation",
"gasUsed": 64745
},
{
"file": "test/WorldProxy.t.sol",
"test": "testCallFromUnlimitedDelegation",
"name": "register an unlimited delegation",
"gasUsed": 79685
},
{
"file": "test/WorldProxy.t.sol",
"test": "testCallFromUnlimitedDelegation",
"name": "call a system via an unlimited delegation",
"gasUsed": 45592
},
{
"file": "test/WorldProxy.t.sol",
"test": "testDeleteRecord",
"name": "Delete record",
"gasUsed": 41626
},
{
"file": "test/WorldProxy.t.sol",
"test": "testPushToDynamicField",
"name": "Push data to the table",
"gasUsed": 115719
},
{
"file": "test/WorldProxy.t.sol",
"test": "testRegisterFunctionSelector",
"name": "Register a function selector",
"gasUsed": 121522
},
{
"file": "test/WorldProxy.t.sol",
"test": "testRegisterNamespace",
"name": "Register a new namespace",
"gasUsed": 148961
},
{
"file": "test/WorldProxy.t.sol",
"test": "testRegisterRootFunctionSelector",
"name": "Register a root function selector",
"gasUsed": 118628
},
{
"file": "test/WorldProxy.t.sol",
"test": "testRegisterSystem",
"name": "register a system",
"gasUsed": 190257
},
{
"file": "test/WorldProxy.t.sol",
"test": "testRegisterTable",
"name": "Register a new table in the namespace",
"gasUsed": 574686
},
{
"file": "test/WorldProxy.t.sol",
"test": "testRenounceNamespace",
"name": "Renounce namespace ownership",
"gasUsed": 67089
},
{
"file": "test/WorldProxy.t.sol",
"test": "testSetField",
"name": "Write data to a table field",
"gasUsed": 65822
},
{
"file": "test/WorldProxy.t.sol",
"test": "testSetRecord",
"name": "Write data to the table",
"gasUsed": 69687
},
{
"file": "test/WorldProxy.t.sol",
"test": "testUnregisterNamespaceDelegation",
"name": "unregister a namespace delegation",
"gasUsed": 65227
},
{
"file": "test/WorldProxy.t.sol",
"test": "testUnregisterUnlimitedDelegation",
"name": "unregister an unlimited delegation",
"gasUsed": 59425
},
{
"file": "test/WorldProxyFactory.t.sol",
"test": "testWorldProxyFactoryGas",
"name": "deploy world via WorldProxyFactory",
"gasUsed": 9032337
},
{
"file": "test/WorldProxyFactory.t.sol",
"test": "testWorldProxyFactoryGas",
"name": "set WorldProxy implementation",
"gasUsed": 31501
},
{
"file": "test/WorldResourceId.t.sol",
"test": "testGetNamespace",
Expand Down
2 changes: 1 addition & 1 deletion packages/world/src/ERC165Checker.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165Checker.sol)
pragma solidity ^0.8.20;
pragma solidity ^0.8.24;

import { IERC165 } from "./IERC165.sol";

Expand Down
24 changes: 24 additions & 0 deletions packages/world/src/IERC1967.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1967.sol)

pragma solidity ^0.8.24;

/**
* @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC.
*/
interface IERC1967 {
/**
* @dev Emitted when the implementation is upgraded.
*/
event Upgraded(address indexed implementation);

/**
* @dev Emitted when the admin account has changed.
*/
event AdminChanged(address previousAdmin, address newAdmin);

/**
* @dev Emitted when the beacon is changed.
*/
event BeaconUpgraded(address indexed beacon);
}
69 changes: 69 additions & 0 deletions packages/world/src/Proxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/Proxy.sol)

pragma solidity ^0.8.24;

/**
* @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM
* instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to
* be specified by overriding the virtual {_implementation} function.
*
* Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a
* different contract through the {_delegate} function.
*
* The success and return data of the delegated call will be returned back to the caller of the proxy.
*/
abstract contract Proxy {
/**
* @dev Delegates the current call to `implementation`.
*
* This function does not return to its internal call site, it will return directly to the external caller.
*/
function _delegate(address implementation) internal virtual {
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())

// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

// Copy the returned data.
returndatacopy(0, 0, returndatasize())

switch result
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}

/**
* @dev This is a virtual function that should be overridden so it returns the address to which the fallback
* function and {_fallback} should delegate.
*/
function _implementation() internal view virtual returns (address);

/**
* @dev Delegates the current call to the address returned by `_implementation()`.
*
* This function does not return to its internal call site, it will return directly to the external caller.
*/
function _fallback() internal virtual {
_delegate(_implementation());
}

/**
* @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other
* function in the contract matches the call data.
*/
fallback() external payable virtual {
_fallback();
}
}
Loading

0 comments on commit 3d1d590

Please sign in to comment.