Skip to content

Commit

Permalink
feat(world-modules): register delegation with signature (#2480)
Browse files Browse the repository at this point in the history
Co-authored-by: Karolis Ramanauskas <[email protected]>
Co-authored-by: alvarius <[email protected]>
  • Loading branch information
3 people authored Mar 26, 2024
1 parent bfd5a01 commit e86bd14
Show file tree
Hide file tree
Showing 23 changed files with 1,037 additions and 27 deletions.
9 changes: 9 additions & 0 deletions .changeset/cold-apes-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@latticexyz/cli": patch
"@latticexyz/world-modules": patch
"@latticexyz/world": patch
---

Added a new preview module, `Unstable_DelegationWithSignatureModule`, which allows registering delegations with a signature.

Note: this module is marked as `Unstable`, because it will be removed and included in the default `World` deployment once it is audited.
3 changes: 2 additions & 1 deletion e2e/packages/client-vanilla/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { setup } from "./mud/setup";
import { decodeEntity } from "@latticexyz/store-sync/recs";

const {
network: { components, latestBlock$, worldContract, waitForTransaction },
network: { components, latestBlock$, walletClient, worldContract, waitForTransaction },
} = await setup();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _window = window as any;
_window.walletClient = walletClient;
_window.worldContract = worldContract;
_window.waitForTransaction = waitForTransaction;

Expand Down
7 changes: 7 additions & 0 deletions e2e/packages/contracts/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,11 @@ export default defineWorld({
key: [],
},
},
modules: [
{
name: "Unstable_DelegationWithSignatureModule",
root: true,
args: [],
},
],
});
108 changes: 108 additions & 0 deletions e2e/packages/sync-test/data/callRegisterDelegationWithSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Page } from "@playwright/test";
import { GetContractReturnType, PublicClient, WalletClient } from "viem";
import { AbiParametersToPrimitiveTypes, ExtractAbiFunction, ExtractAbiFunctionNames } from "abitype";

const DelegationAbi = [
{
type: "function",
name: "registerDelegationWithSignature",
inputs: [
{
name: "delegatee",
type: "address",
internalType: "address",
},
{
name: "delegationControlId",
type: "bytes32",
internalType: "ResourceId",
},
{
name: "initCallData",
type: "bytes",
internalType: "bytes",
},
{
name: "delegator",
type: "address",
internalType: "address",
},
{
name: "signature",
type: "bytes",
internalType: "bytes",
},
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;

type DelegationAbi = typeof DelegationAbi;

type WorldContract = GetContractReturnType<DelegationAbi, PublicClient, WalletClient>;

type WriteMethodName = ExtractAbiFunctionNames<DelegationAbi>;
type WriteMethod<TMethod extends WriteMethodName> = ExtractAbiFunction<DelegationAbi, TMethod>;
type WriteArgs<TMethod extends WriteMethodName> = AbiParametersToPrimitiveTypes<WriteMethod<TMethod>["inputs"]>;

export function callRegisterDelegationWithSignature(page: Page, args?: WriteArgs<"registerDelegationWithSignature">) {
return page.evaluate(
([_args]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const walletClient = (window as any).walletClient as WalletClient;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const worldContract = (window as any).worldContract as WorldContract;

return walletClient
.writeContract({
address: worldContract.address,
abi: [
{
type: "function",
name: "registerDelegationWithSignature",
inputs: [
{
name: "delegatee",
type: "address",
internalType: "address",
},
{
name: "delegationControlId",
type: "bytes32",
internalType: "ResourceId",
},
{
name: "initCallData",
type: "bytes",
internalType: "bytes",
},
{
name: "delegator",
type: "address",
internalType: "address",
},
{
name: "signature",
type: "bytes",
internalType: "bytes",
},
],
outputs: [],
stateMutability: "nonpayable",
},
],
functionName: "registerDelegationWithSignature",
args: _args,
})
.then((tx) => window["waitForTransaction"](tx))
.catch((error) => {
console.error(error);
throw new Error(
[`Error executing registerDelegationWithSignature with args:`, JSON.stringify(_args), error].join("\n\n"),
);
});
},
[args],
);
}
15 changes: 15 additions & 0 deletions e2e/packages/sync-test/data/getWorld.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Page } from "@playwright/test";
import IWorldAbi from "../../contracts/out/IWorld.sol/IWorld.abi.json";
import { GetContractReturnType, PublicClient, WalletClient } from "viem";

type WorldAbi = typeof IWorldAbi;

type WorldContract = GetContractReturnType<WorldAbi, PublicClient, WalletClient>;

export function getWorld(page: Page) {
return page.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const worldContract = (window as any).worldContract as WorldContract;
return worldContract;
}, []);
}
110 changes: 110 additions & 0 deletions e2e/packages/sync-test/registerDelegationWithSignature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { ViteDevServer } from "vite";
import { Browser, Page } from "@playwright/test";
import { createAsyncErrorHandler } from "./asyncErrors";
import { deployContracts, startViteServer, startBrowserAndPage, openClientWithRootAccount } from "./setup";
import { rpcHttpUrl } from "./setup/constants";
import { waitForInitialSync } from "./data/waitForInitialSync";
import { createBurnerAccount, resourceToHex, transportObserver } from "@latticexyz/common";
import { http, createWalletClient, ClientConfig } from "viem";
import { mudFoundry } from "@latticexyz/common/chains";
import { encodeEntity } from "@latticexyz/store-sync/recs";
import { callPageFunction } from "./data/callPageFunction";
import worldConfig from "@latticexyz/world/mud.config";
import { worldToV1 } from "@latticexyz/world/config/v2";
import { delegationWithSignatureTypes } from "@latticexyz/world/internal";
import { getWorld } from "./data/getWorld";
import { callRegisterDelegationWithSignature } from "./data/callRegisterDelegationWithSignature";

const DELEGATOR_PRIVATE_KEY = "0x67bbd1575ecc79b3247c7d7b87a5bc533ccb6a63955a9fefdfaf75853f7cd543";

const worldConfigV1 = worldToV1(worldConfig);

describe("registerDelegationWithSignature", async () => {
const asyncErrorHandler = createAsyncErrorHandler();
let webserver: ViteDevServer;
let browser: Browser;
let page: Page;

beforeEach(async () => {
asyncErrorHandler.resetErrors();

await deployContracts(rpcHttpUrl);

// Start client and browser
webserver = await startViteServer();
const browserAndPage = await startBrowserAndPage(asyncErrorHandler.reportError);
browser = browserAndPage.browser;
page = browserAndPage.page;
});

afterEach(async () => {
await browser.close();
await webserver.close();
});

it("can generate a signature and register a delegation", async () => {
await openClientWithRootAccount(page);
await waitForInitialSync(page);

// Set up client
const clientOptions = {
chain: mudFoundry,
transport: transportObserver(http(mudFoundry.rpcUrls.default.http[0] ?? undefined)),
} as const satisfies ClientConfig;

const delegator = createBurnerAccount(DELEGATOR_PRIVATE_KEY);
const delegatorWalletClient = createWalletClient({
...clientOptions,
account: delegator,
});

const worldContract = await getWorld(page);

// Declare delegation parameters
const delegatee = "0x7203e7ADfDF38519e1ff4f8Da7DCdC969371f377";
const delegationControlId = resourceToHex({ type: "system", namespace: "", name: "unlimited" });
const initCallData = "0x";
const nonce = 0n;

// Sign registration message
const signature = await delegatorWalletClient.signTypedData({
domain: {
chainId: delegatorWalletClient.chain.id,
verifyingContract: worldContract.address,
},
types: delegationWithSignatureTypes,
primaryType: "Delegation",
message: {
delegatee,
delegationControlId,
initCallData,
delegator: delegator.address,
nonce,
},
});

// Register the delegation
await callRegisterDelegationWithSignature(page, [
delegatee,
delegationControlId,
initCallData,
delegator.address,
signature,
]);

// Expect delegation to have been created
const value = await callPageFunction(page, "getComponentValue", [
"UserDelegationControl",
encodeEntity(worldConfigV1.tables.UserDelegationControl.keySchema, {
delegator: delegator.address,
delegatee,
}),
]);

expect(value).toMatchObject({
__staticData: delegationControlId,
delegationControlId,
});
});
});
9 changes: 9 additions & 0 deletions packages/cli/src/utils/defaultModuleContracts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import KeysWithValueModuleData from "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json" assert { type: "json" };
import KeysInTableModuleData from "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json" assert { type: "json" };
import UniqueEntityModuleData from "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json" assert { type: "json" };
// eslint-disable-next-line max-len
import Unstable_DelegationWithSignatureModuleData from "@latticexyz/world-modules/out/Unstable_DelegationWithSignatureModule.sol/Unstable_DelegationWithSignatureModule.json" assert { type: "json" };
import { Abi, Hex, size } from "viem";
import { findPlaceholders } from "./findPlaceholders";

Expand All @@ -27,4 +29,11 @@ export const defaultModuleContracts = [
placeholders: findPlaceholders(UniqueEntityModuleData.bytecode.linkReferences),
deployedBytecodeSize: size(UniqueEntityModuleData.deployedBytecode.object as Hex),
},
{
name: "Unstable_DelegationWithSignatureModule",
abi: Unstable_DelegationWithSignatureModuleData.abi as Abi,
bytecode: Unstable_DelegationWithSignatureModuleData.bytecode.object as Hex,
placeholders: findPlaceholders(Unstable_DelegationWithSignatureModuleData.bytecode.linkReferences),
deployedBytecodeSize: size(Unstable_DelegationWithSignatureModuleData.deployedBytecode.object as Hex),
},
];
18 changes: 15 additions & 3 deletions packages/world-modules/gas-report.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
[
{
"file": "test/DelegationWithSignatureModule.t.sol",
"test": "testInstallRoot",
"name": "install delegation module",
"gasUsed": 689194
},
{
"file": "test/DelegationWithSignatureModule.t.sol",
"test": "testRegisterDelegationWithSignature",
"name": "register an unlimited delegation with signature",
"gasUsed": 117588
},
{
"file": "test/ERC20.t.sol",
"test": "testApprove",
Expand Down Expand Up @@ -267,7 +279,7 @@
"file": "test/StandardDelegationsModule.t.sol",
"test": "testCallFromCallboundDelegation",
"name": "register a callbound delegation",
"gasUsed": 139044
"gasUsed": 138994
},
{
"file": "test/StandardDelegationsModule.t.sol",
Expand All @@ -279,7 +291,7 @@
"file": "test/StandardDelegationsModule.t.sol",
"test": "testCallFromSystemDelegation",
"name": "register a systembound delegation",
"gasUsed": 136166
"gasUsed": 136116
},
{
"file": "test/StandardDelegationsModule.t.sol",
Expand All @@ -291,7 +303,7 @@
"file": "test/StandardDelegationsModule.t.sol",
"test": "testCallFromTimeboundDelegation",
"name": "register a timebound delegation",
"gasUsed": 132709
"gasUsed": 132659
},
{
"file": "test/StandardDelegationsModule.t.sol",
Expand Down
20 changes: 19 additions & 1 deletion packages/world-modules/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,24 @@ export default defineWorld({
tableIdArgument: true,
},
},
/************************************************************************
*
* REGISTER DELEGATION WITH SIGNATURE MODULE
*
************************************************************************/
UserDelegationNonces: {
schema: { delegator: "address", nonce: "uint256" },
key: ["delegator"],
codegen: {
outputDirectory: "modules/delegation/tables",
},
},
},
excludeSystems: ["UniqueEntitySystem", "PuppetFactorySystem", "ERC20System", "ERC721System"],
excludeSystems: [
"UniqueEntitySystem",
"PuppetFactorySystem",
"ERC20System",
"ERC721System",
"Unstable_DelegationWithSignatureSystem",
],
});
1 change: 1 addition & 0 deletions packages/world-modules/src/index.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ import { Owners } from "./modules/erc721-puppet/tables/Owners.sol";
import { TokenApproval } from "./modules/erc721-puppet/tables/TokenApproval.sol";
import { OperatorApproval } from "./modules/erc721-puppet/tables/OperatorApproval.sol";
import { ERC721Registry } from "./modules/erc721-puppet/tables/ERC721Registry.sol";
import { UserDelegationNonces } from "./modules/delegation/tables/UserDelegationNonces.sol";
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

/* Autogenerated file. Do not edit manually. */

import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";

/**
* @title IUnstable_DelegationWithSignatureSystem
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @dev This interface is automatically generated from the corresponding system contract. Do not edit manually.
*/
interface IUnstable_DelegationWithSignatureSystem {
error InvalidSignature(address signer);

function registerDelegationWithSignature(
address delegatee,
ResourceId delegationControlId,
bytes memory initCallData,
address delegator,
bytes memory signature
) external;
}
Loading

0 comments on commit e86bd14

Please sign in to comment.