diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index b489105d18..d663b9561a 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -10,7 +10,7 @@ export const worldAbi = IBaseWorldAbi; // Ideally, this should be an append-only list. Before adding more versions here, be sure to add backwards-compatible support for old Store/World versions. export const supportedStoreVersions = ["2.0.0", "2.0.1", "2.0.2"]; -export const supportedWorldVersions = ["2.0.0", "2.0.1", "2.0.2"]; +export const supportedWorldVersions = ["2.0.0", "2.0.1", "2.0.2", "2.1.0"]; // TODO: extend this to include factory+deployer address? so we can reuse the deployer for a world? export type WorldDeploy = { diff --git a/packages/world-module-callwithsignature/mud.config.ts b/packages/world-module-callwithsignature/mud.config.ts index 24479e3c98..0870297988 100644 --- a/packages/world-module-callwithsignature/mud.config.ts +++ b/packages/world-module-callwithsignature/mud.config.ts @@ -10,4 +10,5 @@ export default defineWorld({ key: ["signer"], }, }, + excludeSystems: ["CallWithSignatureSystem"], }); diff --git a/packages/world-module-callwithsignature/src/codegen/world/IWorld.sol b/packages/world-module-callwithsignature/src/codegen/world/IWorld.sol index 05230cf85d..4761e84790 100644 --- a/packages/world-module-callwithsignature/src/codegen/world/IWorld.sol +++ b/packages/world-module-callwithsignature/src/codegen/world/IWorld.sol @@ -4,7 +4,6 @@ pragma solidity >=0.8.24; /* Autogenerated file. Do not edit manually. */ import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; -import { ICallWithSignatureSystem } from "./ICallWithSignatureSystem.sol"; /** * @title IWorld @@ -13,4 +12,4 @@ import { ICallWithSignatureSystem } from "./ICallWithSignatureSystem.sol"; * that are dynamically registered in the World during deployment. * @dev This is an autogenerated file; do not edit manually. */ -interface IWorld is IBaseWorld, ICallWithSignatureSystem {} +interface IWorld is IBaseWorld {} diff --git a/packages/world/mud.config.ts b/packages/world/mud.config.ts index f5224bc38e..e763ac62c8 100644 --- a/packages/world/mud.config.ts +++ b/packages/world/mud.config.ts @@ -110,6 +110,10 @@ export const tablesConfig = defineWorld({ }, key: [], }, + CallWithSignatureNonces: { + schema: { signer: "address", nonce: "uint256" }, + key: ["signer"], + }, }, }); @@ -147,6 +151,9 @@ export const systemsConfig = defineWorld({ WorldRegistrationSystem: { name: "Registration", }, + CallWithSignatureSystem: { + name: "SignatureCall", + }, }, }); diff --git a/packages/world/src/IWorldErrors.sol b/packages/world/src/IWorldErrors.sol index e702f86695..1aeed7c5aa 100644 --- a/packages/world/src/IWorldErrors.sol +++ b/packages/world/src/IWorldErrors.sol @@ -107,4 +107,9 @@ interface IWorldErrors { * @param functionSelector The function selector of the disallowed callback. */ error World_CallbackNotAllowed(bytes4 functionSelector); + + /** + * @notice Raised when the signature of a call to `callWithSignature` is invalid. + */ + error World_InvalidSignature(); } diff --git a/packages/world/src/codegen/experimental/systems/CallWithSignatureSystemLib.sol b/packages/world/src/codegen/experimental/systems/CallWithSignatureSystemLib.sol new file mode 100644 index 0000000000..eb30510713 --- /dev/null +++ b/packages/world/src/codegen/experimental/systems/CallWithSignatureSystemLib.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { CallWithSignatureSystem } from "../../../modules/init/implementations/CallWithSignatureSystem/CallWithSignatureSystem.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { revertWithBytes } from "../../../revertWithBytes.sol"; +import { IWorldCall } from "../../../IWorldKernel.sol"; +import { SystemCall } from "../../../SystemCall.sol"; +import { WorldContextConsumerLib } from "../../../WorldContext.sol"; +import { Systems } from "../../../codegen/tables/Systems.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + +type CallWithSignatureSystemType is bytes32; + +// equivalent to WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "", name: "SignatureCall" })) +CallWithSignatureSystemType constant callWithSignatureSystem = CallWithSignatureSystemType.wrap( + 0x737900000000000000000000000000005369676e617475726543616c6c000000 +); + +struct CallWrapper { + ResourceId systemId; + address from; +} + +struct RootCallWrapper { + ResourceId systemId; + address from; +} + +/** + * @title CallWithSignatureSystemLib + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This library is automatically generated from the corresponding system contract. Do not edit manually. + */ +library CallWithSignatureSystemLib { + error CallWithSignatureSystemLib_CallingFromRootSystem(); + + function callWithSignature( + CallWithSignatureSystemType self, + address signer, + ResourceId systemId, + bytes memory callData, + bytes memory signature + ) internal returns (bytes memory) { + return CallWrapper(self.toResourceId(), address(0)).callWithSignature(signer, systemId, callData, signature); + } + + function callWithSignature( + CallWrapper memory self, + address signer, + ResourceId systemId, + bytes memory callData, + bytes memory signature + ) internal returns (bytes memory) { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert CallWithSignatureSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall( + _callWithSignature_address_ResourceId_bytes_bytes.callWithSignature, + (signer, systemId, callData, signature) + ); + + bytes memory result = self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + return abi.decode(result, (bytes)); + } + + function callWithSignature( + RootCallWrapper memory self, + address signer, + ResourceId systemId, + bytes memory callData, + bytes memory signature + ) internal returns (bytes memory) { + bytes memory systemCall = abi.encodeCall( + _callWithSignature_address_ResourceId_bytes_bytes.callWithSignature, + (signer, systemId, callData, signature) + ); + + bytes memory result = SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + return abi.decode(result, (bytes)); + } + + function callFrom(CallWithSignatureSystemType self, address from) internal pure returns (CallWrapper memory) { + return CallWrapper(self.toResourceId(), from); + } + + function callAsRoot(CallWithSignatureSystemType self) internal view returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), WorldContextConsumerLib._msgSender()); + } + + function callAsRootFrom( + CallWithSignatureSystemType self, + address from + ) internal pure returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), from); + } + + function toResourceId(CallWithSignatureSystemType self) internal pure returns (ResourceId) { + return ResourceId.wrap(CallWithSignatureSystemType.unwrap(self)); + } + + function fromResourceId(ResourceId resourceId) internal pure returns (CallWithSignatureSystemType) { + return CallWithSignatureSystemType.wrap(resourceId.unwrap()); + } + + function getAddress(CallWithSignatureSystemType self) internal view returns (address) { + return Systems.getSystem(self.toResourceId()); + } + + function _world() private view returns (IWorldCall) { + return IWorldCall(StoreSwitch.getStoreAddress()); + } +} + +/** + * System Function Interfaces + * + * We generate an interface for each system function, which is then used for encoding system calls. + * This is necessary to handle function overloading correctly (which abi.encodeCall cannot). + * + * Each interface is uniquely named based on the function name and parameters to prevent collisions. + */ + +interface _callWithSignature_address_ResourceId_bytes_bytes { + function callWithSignature( + address signer, + ResourceId systemId, + bytes memory callData, + bytes memory signature + ) external; +} + +using CallWithSignatureSystemLib for CallWithSignatureSystemType global; +using CallWithSignatureSystemLib for CallWrapper global; +using CallWithSignatureSystemLib for RootCallWrapper global; diff --git a/packages/world/src/codegen/index.sol b/packages/world/src/codegen/index.sol index b00d58a490..c845d4d753 100644 --- a/packages/world/src/codegen/index.sol +++ b/packages/world/src/codegen/index.sol @@ -15,3 +15,4 @@ import { SystemHooks } from "./tables/SystemHooks.sol"; import { FunctionSelectors } from "./tables/FunctionSelectors.sol"; import { FunctionSignatures } from "./tables/FunctionSignatures.sol"; import { InitModuleAddress } from "./tables/InitModuleAddress.sol"; +import { CallWithSignatureNonces } from "./tables/CallWithSignatureNonces.sol"; diff --git a/packages/world/src/codegen/interfaces/IBaseWorld.sol b/packages/world/src/codegen/interfaces/IBaseWorld.sol index 144be64d0f..6a751ea271 100644 --- a/packages/world/src/codegen/interfaces/IBaseWorld.sol +++ b/packages/world/src/codegen/interfaces/IBaseWorld.sol @@ -9,6 +9,7 @@ import { IRegistrationSystem } from "./IRegistrationSystem.sol"; import { IAccessManagementSystem } from "./IAccessManagementSystem.sol"; import { IBalanceTransferSystem } from "./IBalanceTransferSystem.sol"; import { IBatchCallSystem } from "./IBatchCallSystem.sol"; +import { ICallWithSignatureSystem } from "./ICallWithSignatureSystem.sol"; import { IModuleInstallationSystem } from "./IModuleInstallationSystem.sol"; import { IWorldRegistrationSystem } from "./IWorldRegistrationSystem.sol"; @@ -26,6 +27,7 @@ interface IBaseWorld is IAccessManagementSystem, IBalanceTransferSystem, IBatchCallSystem, + ICallWithSignatureSystem, IModuleInstallationSystem, IWorldRegistrationSystem {} diff --git a/packages/world-module-callwithsignature/src/codegen/world/ICallWithSignatureSystem.sol b/packages/world/src/codegen/interfaces/ICallWithSignatureSystem.sol similarity index 100% rename from packages/world-module-callwithsignature/src/codegen/world/ICallWithSignatureSystem.sol rename to packages/world/src/codegen/interfaces/ICallWithSignatureSystem.sol diff --git a/packages/world/src/codegen/tables/CallWithSignatureNonces.sol b/packages/world/src/codegen/tables/CallWithSignatureNonces.sol new file mode 100644 index 0000000000..941283b504 --- /dev/null +++ b/packages/world/src/codegen/tables/CallWithSignatureNonces.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// Import store internals +import { IStore } from "@latticexyz/store/src/IStore.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { Bytes } from "@latticexyz/store/src/Bytes.sol"; +import { Memory } from "@latticexyz/store/src/Memory.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +library CallWithSignatureNonces { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "world", name: "CallWithSignatur", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x7462776f726c6400000000000000000043616c6c576974685369676e61747572); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of (address) + Schema constant _keySchema = Schema.wrap(0x0014010061000000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (uint256) + Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](1); + keyNames[0] = "signer"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "nonce"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get nonce. + */ + function getNonce(address signer) internal view returns (uint256 nonce) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get nonce. + */ + function _getNonce(address signer) internal view returns (uint256 nonce) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get nonce. + */ + function get(address signer) internal view returns (uint256 nonce) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get nonce. + */ + function _get(address signer) internal view returns (uint256 nonce) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set nonce. + */ + function setNonce(address signer, uint256 nonce) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((nonce)), _fieldLayout); + } + + /** + * @notice Set nonce. + */ + function _setNonce(address signer, uint256 nonce) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((nonce)), _fieldLayout); + } + + /** + * @notice Set nonce. + */ + function set(address signer, uint256 nonce) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((nonce)), _fieldLayout); + } + + /** + * @notice Set nonce. + */ + function _set(address signer, uint256 nonce) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((nonce)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(address signer) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(address signer) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(uint256 nonce) internal pure returns (bytes memory) { + return abi.encodePacked(nonce); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode(uint256 nonce) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData = encodeStatic(nonce); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(address signer) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(signer))); + + return _keyTuple; + } +} diff --git a/packages/world/src/modules/init/InitModule.sol b/packages/world/src/modules/init/InitModule.sol index 417b2f91b3..4a580a61b6 100644 --- a/packages/world/src/modules/init/InitModule.sol +++ b/packages/world/src/modules/init/InitModule.sol @@ -19,10 +19,11 @@ import { NamespaceDelegationControl } from "../../codegen/tables/NamespaceDelega import { AccessManagementSystem } from "./implementations/AccessManagementSystem.sol"; import { BalanceTransferSystem } from "./implementations/BalanceTransferSystem.sol"; import { BatchCallSystem } from "./implementations/BatchCallSystem.sol"; +import { CallWithSignatureSystem } from "./implementations/CallWithSignatureSystem/CallWithSignatureSystem.sol"; import { RegistrationSystem } from "./RegistrationSystem.sol"; -import { ACCESS_MANAGEMENT_SYSTEM_ID, BALANCE_TRANSFER_SYSTEM_ID, BATCH_CALL_SYSTEM_ID, REGISTRATION_SYSTEM_ID } from "./constants.sol"; -import { getFunctionSignaturesAccessManagement, getFunctionSignaturesBalanceTransfer, getFunctionSignaturesBatchCall, getFunctionSignaturesRegistration } from "./functionSignatures.sol"; +import { ACCESS_MANAGEMENT_SYSTEM_ID, BALANCE_TRANSFER_SYSTEM_ID, BATCH_CALL_SYSTEM_ID, REGISTRATION_SYSTEM_ID, CALL_WITH_SIGNATURE_SYSTEM_ID } from "./constants.sol"; +import { getFunctionSignaturesAccessManagement, getFunctionSignaturesBalanceTransfer, getFunctionSignaturesBatchCall, getFunctionSignaturesRegistration, getFunctionSignaturesCallWithSignature } from "./functionSignatures.sol"; import { Systems } from "../../codegen/tables/Systems.sol"; import { FunctionSelectors } from "../../codegen/tables/FunctionSelectors.sol"; @@ -31,6 +32,7 @@ import { SystemHooks } from "../../codegen/tables/SystemHooks.sol"; import { SystemRegistry } from "../../codegen/tables/SystemRegistry.sol"; import { InitModuleAddress } from "../../codegen/tables/InitModuleAddress.sol"; import { Balances } from "../../codegen/tables/Balances.sol"; +import { CallWithSignatureNonces } from "../../codegen/tables/CallWithSignatureNonces.sol"; import { WorldRegistrationSystem } from "./implementations/WorldRegistrationSystem.sol"; @@ -45,17 +47,20 @@ contract InitModule is Module { address internal immutable balanceTransferSystem; address internal immutable batchCallSystem; address internal immutable registrationSystem; + address internal immutable delegationSystem; constructor( AccessManagementSystem _accessManagementSystem, BalanceTransferSystem _balanceTransferSystem, BatchCallSystem _batchCallSystem, - RegistrationSystem _registrationSystem + RegistrationSystem _registrationSystem, + CallWithSignatureSystem _delegationSystem ) { accessManagementSystem = address(_accessManagementSystem); balanceTransferSystem = address(_balanceTransferSystem); batchCallSystem = address(_batchCallSystem); registrationSystem = address(_registrationSystem); + delegationSystem = address(_delegationSystem); } /** @@ -86,6 +91,7 @@ contract InitModule is Module { SystemHooks.register(); SystemRegistry.register(); InitModuleAddress.register(); + CallWithSignatureNonces.register(); ResourceIds._setExists(ROOT_NAMESPACE_ID, true); NamespaceOwner._set(ROOT_NAMESPACE_ID, _msgSender()); @@ -108,6 +114,7 @@ contract InitModule is Module { _registerSystem(balanceTransferSystem, BALANCE_TRANSFER_SYSTEM_ID); _registerSystem(batchCallSystem, BATCH_CALL_SYSTEM_ID); _registerSystem(registrationSystem, REGISTRATION_SYSTEM_ID); + _registerSystem(delegationSystem, CALL_WITH_SIGNATURE_SYSTEM_ID); } /** @@ -147,6 +154,11 @@ contract InitModule is Module { for (uint256 i = 0; i < functionSignaturesRegistration.length; i++) { _registerRootFunctionSelector(REGISTRATION_SYSTEM_ID, functionSignaturesRegistration[i]); } + + string[1] memory functionSignaturesCallWithSignature = getFunctionSignaturesCallWithSignature(); + for (uint256 i = 0; i < functionSignaturesCallWithSignature.length; i++) { + _registerRootFunctionSelector(CALL_WITH_SIGNATURE_SYSTEM_ID, functionSignaturesCallWithSignature[i]); + } } /** diff --git a/packages/world/src/modules/init/constants.sol b/packages/world/src/modules/init/constants.sol index e17ab3635a..96db868ac8 100644 --- a/packages/world/src/modules/init/constants.sol +++ b/packages/world/src/modules/init/constants.sol @@ -37,3 +37,11 @@ ResourceId constant BATCH_CALL_SYSTEM_ID = ResourceId.wrap( ResourceId constant REGISTRATION_SYSTEM_ID = ResourceId.wrap( bytes32(abi.encodePacked(RESOURCE_SYSTEM, ROOT_NAMESPACE, bytes16("Registration"))) ); + +/** + * @dev Resource ID for the call with signature system. + * @dev This ID is derived from the RESOURCE_SYSTEM type, the ROOT_NAMESPACE, and the system name. + */ +ResourceId constant CALL_WITH_SIGNATURE_SYSTEM_ID = ResourceId.wrap( + (bytes32(abi.encodePacked(RESOURCE_SYSTEM, ROOT_NAMESPACE, bytes16("CallWithSignatur")))) +); diff --git a/packages/world/src/modules/init/functionSignatures.sol b/packages/world/src/modules/init/functionSignatures.sol index e9a7fe13bd..c11df904cd 100644 --- a/packages/world/src/modules/init/functionSignatures.sol +++ b/packages/world/src/modules/init/functionSignatures.sol @@ -60,3 +60,13 @@ function getFunctionSignaturesRegistration() pure returns (string[14] memory) { "unregisterNamespaceDelegation(bytes32)" ]; } + +/** + * @dev Function signatures for call with signature system + */ +function getFunctionSignaturesCallWithSignature() pure returns (string[1] memory) { + return [ + // --- CallWithSignatureSystem --- + "callWithSignature(address,bytes32,bytes,bytes)" + ]; +} diff --git a/packages/world/src/modules/init/implementations/CallWithSignatureSystem/CallWithSignatureSystem.sol b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/CallWithSignatureSystem.sol new file mode 100644 index 0000000000..e564aca8ee --- /dev/null +++ b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/CallWithSignatureSystem.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +import { System } from "../../../../System.sol"; +import { SystemCall } from "../../../../SystemCall.sol"; +import { CallWithSignatureNonces } from "../../../../codegen/tables/CallWithSignatureNonces.sol"; +import { IWorldErrors } from "../../../../IWorldErrors.sol"; +import { LimitedCallContext } from "../../LimitedCallContext.sol"; +import { createDelegation } from "../createDelegation.sol"; +import { getSignedMessageHash } from "./getSignedMessageHash.sol"; +import { ECDSA } from "./ECDSA.sol"; +import { validateCallWithSignature } from "./validateCallWithSignature.sol"; + +contract CallWithSignatureSystem is System, IWorldErrors, LimitedCallContext { + /** + * @notice Calls a system with a given system ID using the given signature. + * @param signer The address on whose behalf the system is called. + * @param systemId The ID of the system to be called. + * @param callData The ABI data for the system call. + * @param signature The EIP712 signature. + * @return Return data from the system call. + */ + function callWithSignature( + address signer, + ResourceId systemId, + bytes memory callData, + bytes memory signature + ) public payable onlyDelegatecall returns (bytes memory) { + validateCallWithSignature(signer, systemId, callData, signature); + + CallWithSignatureNonces._set(signer, CallWithSignatureNonces._get(signer) + 1); + + return SystemCall.callWithHooksOrRevert(signer, systemId, callData, _msgValue()); + } +} diff --git a/packages/world/src/modules/init/implementations/CallWithSignatureSystem/ECDSA.sol b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/ECDSA.sol new file mode 100644 index 0000000000..01201021db --- /dev/null +++ b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/ECDSA.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) + +pragma solidity >=0.8.24; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS + } + + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + /** + * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not + * return address(0) without also returning an error description. Errors are documented using an enum (error type) + * and a bytes32 providing additional information about the error. + * + * If no error is returned, then the address can be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + */ + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures] + */ + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError, bytes32) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS, s); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature, bytes32(0)); + } + + return (signer, RecoverError.NoError, bytes32(0)); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. + */ + function _throwError(RecoverError error, bytes32 errorArg) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert ECDSAInvalidSignature(); + } else if (error == RecoverError.InvalidSignatureLength) { + revert ECDSAInvalidSignatureLength(uint256(errorArg)); + } else if (error == RecoverError.InvalidSignatureS) { + revert ECDSAInvalidSignatureS(errorArg); + } + } +} diff --git a/packages/world/src/modules/init/implementations/CallWithSignatureSystem/IERC1271.sol b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/IERC1271.sol new file mode 100644 index 0000000000..a1fd71358a --- /dev/null +++ b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/IERC1271.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1271.sol) + +pragma solidity ^0.8.24; + +/** + * @dev Interface of the ERC-1271 standard signature validation method for + * contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271]. + */ +interface IERC1271 { + /** + * @dev Should return whether the signature provided is valid for the provided data + * @param hash Hash of the data to be signed + * @param signature Signature byte array associated with _data + */ + function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); +} diff --git a/packages/world/src/modules/init/implementations/CallWithSignatureSystem/SignatureChecker.sol b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/SignatureChecker.sol new file mode 100644 index 0000000000..cbadd6d679 --- /dev/null +++ b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/SignatureChecker.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/SignatureChecker.sol) + +pragma solidity ^0.8.24; + +import { ECDSA } from "./ECDSA.sol"; +import { IERC1271 } from "./IERC1271.sol"; + +/** + * @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA + * signatures from externally owned accounts (EOAs) as well as ERC-1271 signatures from smart contract wallets like + * Argent and Safe Wallet (previously Gnosis Safe). + */ +library SignatureChecker { + /** + * @dev Checks if a signature is valid for a given signer and data hash. If the signer is a smart contract, the + * signature is validated against that smart contract using ERC-1271, otherwise it's validated using `ECDSA.recover`. + * + * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus + * change through time. It could return true at block N and false at block N+1 (or the opposite). + */ + function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + if (signer.code.length == 0) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); + return err == ECDSA.RecoverError.NoError && recovered == signer; + } else { + return isValidERC1271SignatureNow(signer, hash, signature); + } + } + + /** + * @dev Checks if a signature is valid for a given signer and data hash. The signature is validated + * against the signer smart contract using ERC-1271. + * + * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus + * change through time. It could return true at block N and false at block N+1 (or the opposite). + */ + function isValidERC1271SignatureNow( + address signer, + bytes32 hash, + bytes memory signature + ) internal view returns (bool) { + (bool success, bytes memory result) = signer.staticcall( + abi.encodeCall(IERC1271.isValidSignature, (hash, signature)) + ); + return (success && + result.length >= 32 && + abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); + } +} diff --git a/packages/world/src/modules/init/implementations/CallWithSignatureSystem/getSignedMessageHash.sol b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/getSignedMessageHash.sol new file mode 100644 index 0000000000..57048a9c2e --- /dev/null +++ b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/getSignedMessageHash.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "../../../../WorldResourceId.sol"; + +using WorldResourceIdInstance for ResourceId; + +// Note the intended value of the `salt` field is the chain ID. +// It is not included in `chainId`, to allow cross-chain signing without requiring wallets to switch networks. +// The value of this field should be the chain on which the world lives, rather than the chain the wallet is connected to. +bytes32 constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(address verifyingContract,bytes32 salt)"); +bytes32 constant CALL_TYPEHASH = keccak256( + "Call(address signer,string systemNamespace,string systemName,bytes callData,uint256 nonce)" +); + +/** + * @notice Generate the message hash for a given delegation signature. + * For EIP712 signatures https://eips.ethereum.org/EIPS/eip-712 + * @dev We include the signer address to prevent generating a signature that recovers to a random address that didn't sign the message. + * @param signer The address on whose behalf the system is called. + * @param systemId The ID of the system to be called. + * @param callData The ABI data for the system call. + * @param nonce The nonce of the signer + * @param worldAddress The world address + * @return Return the message hash. + */ +function getSignedMessageHash( + address signer, + ResourceId systemId, + bytes memory callData, + uint256 nonce, + address worldAddress +) view returns (bytes32) { + bytes32 domainSeperator = keccak256(abi.encode(DOMAIN_TYPEHASH, worldAddress, bytes32(block.chainid))); + + return + keccak256( + abi.encodePacked( + "\x19\x01", + domainSeperator, + keccak256( + abi.encode( + CALL_TYPEHASH, + signer, + keccak256(bytes(WorldResourceIdLib.toTrimmedString(systemId.getNamespace()))), + keccak256(bytes(WorldResourceIdLib.toTrimmedString(systemId.getName()))), + keccak256(callData), + nonce + ) + ) + ) + ); +} diff --git a/packages/world/src/modules/init/implementations/CallWithSignatureSystem/validateCallWithSignature.sol b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/validateCallWithSignature.sol new file mode 100644 index 0000000000..f3a95a24c0 --- /dev/null +++ b/packages/world/src/modules/init/implementations/CallWithSignatureSystem/validateCallWithSignature.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; +import { ResourceId } from "../../../../WorldResourceId.sol"; +import { WorldContextConsumerLib } from "../../../../WorldContext.sol"; +import { CallWithSignatureNonces } from "../../../../codegen/tables/CallWithSignatureNonces.sol"; +import { IWorldErrors } from "../../../../IWorldErrors.sol"; +import { getSignedMessageHash } from "./getSignedMessageHash.sol"; +import { ECDSA } from "./ECDSA.sol"; +import { SignatureChecker } from "./SignatureChecker.sol"; + +/** + * @notice Verifies the given system call corresponds to the given signature. + * @param signer The address on whose behalf the system is called. + * @param systemId The ID of the system to be called. + * @param callData The ABI data for the system call. + * @param signature The EIP712 signature. + * @dev Reverts with InvalidSignature(recoveredSigner) if the signature is invalid. + */ +function validateCallWithSignature( + address signer, + ResourceId systemId, + bytes memory callData, + bytes memory signature +) view { + uint256 nonce = CallWithSignatureNonces._get(signer); + bytes32 hash = getSignedMessageHash(signer, systemId, callData, nonce, WorldContextConsumerLib._world()); + + if (!SignatureChecker.isValidSignatureNow(signer, hash, signature)) { + revert IWorldErrors.World_InvalidSignature(); + } +} diff --git a/packages/world/test/InitSystems.t.sol b/packages/world/test/InitSystems.t.sol index 99c9714911..d0623118ed 100644 --- a/packages/world/test/InitSystems.t.sol +++ b/packages/world/test/InitSystems.t.sol @@ -9,9 +9,9 @@ import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; import { createWorld } from "./createWorld.sol"; import { LimitedCallContext } from "../src/modules/init/LimitedCallContext.sol"; -import { getFunctionSignaturesAccessManagement, getFunctionSignaturesBalanceTransfer, getFunctionSignaturesBatchCall, getFunctionSignaturesRegistration } from "../src/modules/init/functionSignatures.sol"; +import { getFunctionSignaturesAccessManagement, getFunctionSignaturesBalanceTransfer, getFunctionSignaturesBatchCall, getFunctionSignaturesRegistration, getFunctionSignaturesCallWithSignature } from "../src/modules/init/functionSignatures.sol"; -import { ACCESS_MANAGEMENT_SYSTEM_ID, BALANCE_TRANSFER_SYSTEM_ID, BATCH_CALL_SYSTEM_ID, REGISTRATION_SYSTEM_ID } from "../src/modules/init/constants.sol"; +import { ACCESS_MANAGEMENT_SYSTEM_ID, BALANCE_TRANSFER_SYSTEM_ID, BATCH_CALL_SYSTEM_ID, REGISTRATION_SYSTEM_ID, CALL_WITH_SIGNATURE_SYSTEM_ID } from "../src/modules/init/constants.sol"; import { Systems } from "../src/codegen/tables/Systems.sol"; @@ -69,4 +69,12 @@ contract LimitedCallContextTest is Test { callSystem(REGISTRATION_SYSTEM_ID, functionSignaturesRegistration[i]); } } + + function testCallWithSignatureSystem() public { + string[1] memory functionSignaturesCallWithSignature = getFunctionSignaturesCallWithSignature(); + + for (uint256 i; i < functionSignaturesCallWithSignature.length; i++) { + callSystem(CALL_WITH_SIGNATURE_SYSTEM_ID, functionSignaturesCallWithSignature[i]); + } + } } diff --git a/packages/world/test/createInitModule.sol b/packages/world/test/createInitModule.sol index 8354584c5a..fba7b3db73 100644 --- a/packages/world/test/createInitModule.sol +++ b/packages/world/test/createInitModule.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.24; import { AccessManagementSystem } from "../src/modules/init/implementations/AccessManagementSystem.sol"; import { BalanceTransferSystem } from "../src/modules/init/implementations/BalanceTransferSystem.sol"; import { BatchCallSystem } from "../src/modules/init/implementations/BatchCallSystem.sol"; +import { CallWithSignatureSystem } from "../src/modules/init/implementations/CallWithSignatureSystem/CallWithSignatureSystem.sol"; import { InitModule } from "../src/modules/init/InitModule.sol"; import { RegistrationSystem } from "../src/modules/init/RegistrationSystem.sol"; @@ -14,6 +15,7 @@ function createInitModule() returns (InitModule) { new AccessManagementSystem(), new BalanceTransferSystem(), new BatchCallSystem(), - new RegistrationSystem() + new RegistrationSystem(), + new CallWithSignatureSystem() ); } diff --git a/packages/world/ts/protocol-snapshots/2.0.2.snap b/packages/world/ts/protocol-snapshots/2.0.2.snap index aed3400889..cf9a91cf58 100644 --- a/packages/world/ts/protocol-snapshots/2.0.2.snap +++ b/packages/world/ts/protocol-snapshots/2.0.2.snap @@ -37,6 +37,7 @@ "error World_InvalidNamespace(bytes14 namespace)", "error World_InvalidResourceId(bytes32 resourceId, string resourceIdString)", "error World_InvalidResourceType(bytes2 expected, bytes32 resourceId, string resourceIdString)", + "error World_InvalidSignature()", "error World_ResourceAlreadyExists(bytes32 resourceId, string resourceIdString)", "error World_ResourceNotFound(bytes32 resourceId, string resourceIdString)", "error World_SystemAlreadyExists(address system)", @@ -51,6 +52,7 @@ "function batchCallFrom((address from, bytes32 systemId, bytes callData)[] systemCalls) returns (bytes[] returnDatas)", "function call(bytes32 systemId, bytes callData) payable returns (bytes)", "function callFrom(address delegator, bytes32 systemId, bytes callData) payable returns (bytes)", + "function callWithSignature(address signer, bytes32 systemId, bytes callData, bytes signature) payable returns (bytes)", "function creator() view returns (address)", "function deleteRecord(bytes32 tableId, bytes32[] keyTuple)", "function getDynamicField(bytes32 tableId, bytes32[] keyTuple, uint8 dynamicFieldIndex) view returns (bytes)", diff --git a/packages/world/ts/protocolVersions.ts b/packages/world/ts/protocolVersions.ts index b91dcbbbc2..3884514361 100644 --- a/packages/world/ts/protocolVersions.ts +++ b/packages/world/ts/protocolVersions.ts @@ -1,5 +1,7 @@ // History of protocol versions and a short description of what changed in each. export const protocolVersions = { + "2.1.0": + "Added `callWithSignature` to allow calling functions on behalf of another address by providing a signature.", "2.0.2": "Patched `StoreCore.registerTable` to prevent registering both an offchain and onchain table with the same name.", "2.0.1": "Patched `StoreRead.getDynamicFieldLength` to use the correct method to read the dynamic field length.",