diff --git a/packages/world-modules/gas-report.json b/packages/world-modules/gas-report.json index 45d712c6c5..33c0de667b 100644 --- a/packages/world-modules/gas-report.json +++ b/packages/world-modules/gas-report.json @@ -75,13 +75,13 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallComposite", "name": "install keys in table module", - "gasUsed": 1438969 + "gasUsed": 1439079 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1438969 + "gasUsed": 1439079 }, { "file": "test/KeysInTableModule.t.sol", @@ -93,13 +93,13 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1438969 + "gasUsed": 1439079 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1438969 + "gasUsed": 1439079 }, { "file": "test/KeysInTableModule.t.sol", @@ -117,7 +117,7 @@ "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1438969 + "gasUsed": 1439079 }, { "file": "test/KeysInTableModule.t.sol", @@ -135,7 +135,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 684371 + "gasUsed": 684459 }, { "file": "test/KeysWithValueModule.t.sol", @@ -153,7 +153,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 684371 + "gasUsed": 684459 }, { "file": "test/KeysWithValueModule.t.sol", @@ -165,7 +165,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 684371 + "gasUsed": 684459 }, { "file": "test/KeysWithValueModule.t.sol", @@ -183,7 +183,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 684371 + "gasUsed": 684459 }, { "file": "test/KeysWithValueModule.t.sol", @@ -267,7 +267,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromCallboundDelegation", "name": "register a callbound delegation", - "gasUsed": 118175 + "gasUsed": 118198 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -279,7 +279,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromSystemDelegation", "name": "register a systembound delegation", - "gasUsed": 115728 + "gasUsed": 115751 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -291,7 +291,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromTimeboundDelegation", "name": "register a timebound delegation", - "gasUsed": 112651 + "gasUsed": 112674 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -303,7 +303,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 704240 + "gasUsed": 704219 }, { "file": "test/UniqueEntityModule.t.sol", @@ -315,7 +315,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 673144 + "gasUsed": 673145 }, { "file": "test/UniqueEntityModule.t.sol", diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index 4ea750e7ae..54da86f966 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -63,7 +63,7 @@ "file": "test/Factories.t.sol", "test": "testWorldFactory", "name": "deploy world via WorldFactory", - "gasUsed": 12512902 + "gasUsed": 12642949 }, { "file": "test/World.t.sol", @@ -81,7 +81,7 @@ "file": "test/World.t.sol", "test": "testCallFromUnlimitedDelegation", "name": "register an unlimited delegation", - "gasUsed": 47584 + "gasUsed": 47607 }, { "file": "test/World.t.sol", @@ -105,7 +105,7 @@ "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 84519 + "gasUsed": 84542 }, { "file": "test/World.t.sol", @@ -117,13 +117,13 @@ "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 80467 + "gasUsed": 80445 }, { "file": "test/World.t.sol", "test": "testRegisterSystem", "name": "register a system", - "gasUsed": 164385 + "gasUsed": 164363 }, { "file": "test/World.t.sol", @@ -131,6 +131,12 @@ "name": "Register a new table in the namespace", "gasUsed": 536881 }, + { + "file": "test/World.t.sol", + "test": "testRenounceNamespace", + "name": "Renounce namespace ownership", + "gasUsed": 36773 + }, { "file": "test/World.t.sol", "test": "testSetField", @@ -143,11 +149,17 @@ "name": "Write data to the table", "gasUsed": 39047 }, + { + "file": "test/World.t.sol", + "test": "testUnregisterNamespaceDelegation", + "name": "unregister a namespace delegation", + "gasUsed": 28360 + }, { "file": "test/World.t.sol", "test": "testUnregisterUnlimitedDelegation", "name": "unregister an unlimited delegation", - "gasUsed": 26173 + "gasUsed": 26255 }, { "file": "test/WorldDynamicUpdate.t.sol", diff --git a/packages/world/src/codegen/interfaces/IAccessManagementSystem.sol b/packages/world/src/codegen/interfaces/IAccessManagementSystem.sol index 0188232be4..187b16baa6 100644 --- a/packages/world/src/codegen/interfaces/IAccessManagementSystem.sol +++ b/packages/world/src/codegen/interfaces/IAccessManagementSystem.sol @@ -15,4 +15,6 @@ interface IAccessManagementSystem { function revokeAccess(ResourceId resourceId, address grantee) external; function transferOwnership(ResourceId namespaceId, address newOwner) external; + + function renounceOwnership(ResourceId namespaceId) external; } diff --git a/packages/world/src/codegen/interfaces/IWorldRegistrationSystem.sol b/packages/world/src/codegen/interfaces/IWorldRegistrationSystem.sol index 95523a733e..3121520454 100644 --- a/packages/world/src/codegen/interfaces/IWorldRegistrationSystem.sol +++ b/packages/world/src/codegen/interfaces/IWorldRegistrationSystem.sol @@ -40,4 +40,6 @@ interface IWorldRegistrationSystem { ResourceId delegationControlId, bytes memory initCallData ) external; + + function unregisterNamespaceDelegation(ResourceId namespaceId) external; } diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol index 83a91af3e8..0637cd92f2 100644 --- a/packages/world/src/modules/core/CoreModule.sol +++ b/packages/world/src/modules/core/CoreModule.sol @@ -141,11 +141,12 @@ contract CoreModule is Module { * @dev Iterates through known function signatures and registers them. */ function _registerFunctionSelectors() internal { - string[3] memory functionSignaturesAccessManagement = [ + string[4] memory functionSignaturesAccessManagement = [ // --- AccessManagementSystem --- "grantAccess(bytes32,address)", "revokeAccess(bytes32,address)", - "transferOwnership(bytes32,address)" + "transferOwnership(bytes32,address)", + "renounceOwnership(bytes32)" ]; for (uint256 i = 0; i < functionSignaturesAccessManagement.length; i++) { _registerRootFunctionSelector(ACCESS_MANAGEMENT_SYSTEM_ID, functionSignaturesAccessManagement[i]); @@ -169,7 +170,7 @@ contract CoreModule is Module { _registerRootFunctionSelector(BATCH_CALL_SYSTEM_ID, functionSignaturesBatchCall[i]); } - string[13] memory functionSignaturesCoreRegistration = [ + string[14] memory functionSignaturesCoreRegistration = [ // --- ModuleInstallationSystem --- "installModule(address,bytes)", // --- StoreRegistrationSystem --- @@ -185,7 +186,8 @@ contract CoreModule is Module { "registerRootFunctionSelector(bytes32,string,bytes4)", "registerDelegation(address,bytes32,bytes)", "unregisterDelegation(address)", - "registerNamespaceDelegation(bytes32,bytes32,bytes)" + "registerNamespaceDelegation(bytes32,bytes32,bytes)", + "unregisterNamespaceDelegation(bytes32)" ]; for (uint256 i = 0; i < functionSignaturesCoreRegistration.length; i++) { _registerRootFunctionSelector(CORE_REGISTRATION_SYSTEM_ID, functionSignaturesCoreRegistration[i]); diff --git a/packages/world/src/modules/core/implementations/AccessManagementSystem.sol b/packages/world/src/modules/core/implementations/AccessManagementSystem.sol index 6d9c5c6d99..51816fd70a 100644 --- a/packages/world/src/modules/core/implementations/AccessManagementSystem.sol +++ b/packages/world/src/modules/core/implementations/AccessManagementSystem.sol @@ -53,7 +53,7 @@ contract AccessManagementSystem is System, LimitedCallContext { * @param newOwner The address to which ownership should be transferred. */ function transferOwnership(ResourceId namespaceId, address newOwner) public virtual onlyDelegatecall { - // Require the namespace to be a valid namespace ID + // Require the namespace ID to be a valid namespace requireNamespace(namespaceId); // Require the namespace to exist @@ -71,4 +71,26 @@ contract AccessManagementSystem is System, LimitedCallContext { // Grant access to new owner ResourceAccess._set(namespaceId, newOwner, true); } + + /** + * @notice Renounces ownership of the given namespace + * @dev Requires the caller to own the namespace. Revoke ResourceAccess for previous owner + * @param namespaceId The ID of the namespace to transfer ownership. + */ + function renounceOwnership(ResourceId namespaceId) public virtual onlyDelegatecall { + // Require the namespace ID to be a valid namespace + requireNamespace(namespaceId); + + // Require the namespace to exist + AccessControl.requireExistence(namespaceId); + + // Require the caller to own the namespace + AccessControl.requireOwner(namespaceId, _msgSender()); + + // Delete namespace owner + NamespaceOwner._deleteRecord(namespaceId); + + // Revoke access from old owner + ResourceAccess._deleteRecord(namespaceId, _msgSender()); + } } diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol index 5b3fc1c526..616da52738 100644 --- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol @@ -44,7 +44,7 @@ contract WorldRegistrationSystem is System, IWorldErrors, LimitedCallContext { * @param namespaceId The unique identifier for the new namespace */ function registerNamespace(ResourceId namespaceId) public virtual onlyDelegatecall { - // Require namespace to be a valid namespace ID + // Require namespace ID to be a valid namespace requireNamespace(namespaceId); // Require namespace to not exist yet @@ -284,7 +284,12 @@ contract WorldRegistrationSystem is System, IWorldErrors, LimitedCallContext { } } - function unregisterDelegation(address delegatee) public { + /** + * @notice Unregisters a delegation + * @dev Deletes the new delegation from the caller to the specified delegatee + * @param delegatee The address of the delegatee + */ + function unregisterDelegation(address delegatee) public onlyDelegatecall { // Delete the delegation control contract address UserDelegationControl.deleteRecord({ delegator: _msgSender(), delegatee: delegatee }); } @@ -301,7 +306,7 @@ contract WorldRegistrationSystem is System, IWorldErrors, LimitedCallContext { ResourceId delegationControlId, bytes memory initCallData ) public onlyDelegatecall { - // Require namespace to be a valid namespace ID + // Require namespace ID to be a valid namespace requireNamespace(namespaceId); // Require the delegation to not be unlimited @@ -329,4 +334,23 @@ contract WorldRegistrationSystem is System, IWorldErrors, LimitedCallContext { }); } } + + /** + * @notice Unregisters a delegation for a namespace + * @dev Deletes the delegation control for a specific namespace + * @param namespaceId The ID of the namespace + */ + function unregisterNamespaceDelegation(ResourceId namespaceId) public onlyDelegatecall { + // Require namespace ID to be a valid namespace + requireNamespace(namespaceId); + + // Require the namespace to exist + AccessControl.requireExistence(namespaceId); + + // Require the caller to own the namespace + AccessControl.requireOwner(namespaceId, _msgSender()); + + // Delete the delegation control + NamespaceDelegationControl.deleteRecord(namespaceId); + } } diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index 1dac97b29d..83afa4b809 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -225,11 +225,12 @@ contract WorldTest is Test, GasReporter { CoreRegistrationSystem coreRegistrationSystem = CoreRegistrationSystem( Systems.getSystem(CORE_REGISTRATION_SYSTEM_ID) ); - bytes4[19] memory coreFunctionSignatures = [ + bytes4[22] memory coreFunctionSignatures = [ // --- AccessManagementSystem --- AccessManagementSystem.grantAccess.selector, AccessManagementSystem.revokeAccess.selector, AccessManagementSystem.transferOwnership.selector, + AccessManagementSystem.renounceOwnership.selector, // --- BalanceTransferSystem --- BalanceTransferSystem.transferBalanceToNamespace.selector, BalanceTransferSystem.transferBalanceToAddress.selector, @@ -250,7 +251,9 @@ contract WorldTest is Test, GasReporter { coreRegistrationSystem.registerFunctionSelector.selector, coreRegistrationSystem.registerRootFunctionSelector.selector, coreRegistrationSystem.registerDelegation.selector, - coreRegistrationSystem.registerNamespaceDelegation.selector + coreRegistrationSystem.unregisterDelegation.selector, + coreRegistrationSystem.registerNamespaceDelegation.selector, + coreRegistrationSystem.unregisterNamespaceDelegation.selector ]; for (uint256 i; i < coreFunctionSignatures.length; i++) { @@ -426,6 +429,33 @@ contract WorldTest is Test, GasReporter { world.transferOwnership(namespaceId, address(1)); } + function testRenounceNamespace() public { + bytes14 namespace = "testRenounce"; + ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(namespace); + + world.registerNamespace(namespaceId); + + // Expect the new owner to not be namespace owner before transfer + assertFalse( + (NamespaceOwner.get(namespaceId)) == address(0), + "new owner should not be namespace owner before transfer" + ); + + startGasReport("Renounce namespace ownership"); + world.renounceOwnership(namespaceId); + endGasReport(); + + // Expect the new owner to be zero address + assertEq(NamespaceOwner.get(namespaceId), address(0), "zero address should be namespace owner"); + + // Expect previous owner to no longer have access + assertEq(ResourceAccess.get(namespaceId, address(this)), false, "caller should no longer have access"); + + // Expect revert if caller is not the owner + _expectAccessDenied(address(this), namespace, 0, RESOURCE_NAMESPACE); + world.renounceOwnership(namespaceId); + } + function testRegisterTable() public { FieldLayout fieldLayout = FieldLayoutEncodeHelper.encode(1, 32, 1); Schema valueSchema = SchemaEncodeHelper.encode(SchemaType.BOOL, SchemaType.UINT256, SchemaType.STRING); @@ -1155,6 +1185,69 @@ contract WorldTest is Test, GasReporter { world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); } + function testUnregisterNamespaceDelegation() public { + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + ResourceId systemId = WorldResourceIdLib.encode({ + typeId: RESOURCE_SYSTEM, + namespace: "namespace", + name: "testSystem" + }); + world.registerNamespace(systemId.getNamespaceId()); + world.registerSystem(systemId, system, true); + + // Register a delegation control mock system + DelegationControlMock delegationControlMock = new DelegationControlMock(); + ResourceId delegationControlMockId = WorldResourceIdLib.encode({ + typeId: RESOURCE_SYSTEM, + namespace: "delegation", + name: "mock" + }); + world.registerNamespace(delegationControlMockId.getNamespaceId()); + world.registerSystem(delegationControlMockId, delegationControlMock, true); + + address delegator = address(1); + address delegatee = address(2); + ResourceId namespaceId = systemId.getNamespaceId(); + + // Expect a revert when attempting to perform a call via callFrom before a delegation was created + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.World_DelegationNotFound.selector, delegator, delegatee)); + vm.prank(delegatee); + world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); + + // Register the delegation mock as the delegation control for the namespace + // with `delegatee` and `namespaceId` in the init call data + world.registerNamespaceDelegation( + namespaceId, + delegationControlMockId, + abi.encodeWithSelector(delegationControlMock.initDelegation.selector, namespaceId, delegatee) + ); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + bytes memory returnData = world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + + // Unregister the delegation + startGasReport("unregister a namespace delegation"); + world.unregisterNamespaceDelegation(namespaceId); + endGasReport(); + + // Expect a revert when attempting to perform a call on behalf of the previous delegatee + vm.expectRevert( + abi.encodeWithSelector( + IWorldErrors.World_DelegationNotFound.selector, + delegator, + delegatee // Invalid delegatee + ) + ); + vm.prank(delegatee); + world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); + } + function testRegisterStoreHook() public { FieldLayout fieldLayout = Bool.getFieldLayout(); Schema valueSchema = Bool.getValueSchema();