Skip to content

Commit 041031d

Browse files
authored
refactor(world-consumer): remove namespace (#3597)
1 parent 581228b commit 041031d

15 files changed

Lines changed: 335 additions & 190 deletions

File tree

.changeset/rich-islands-stare.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@latticexyz/world-consumer": patch
3+
"@latticexyz/world-module-erc20": patch
4+
---
5+
6+
`WorldConsumer` now doesn't store a single namespace. Instead, child contracts can keep track of namespaces and use the `onlyNamespace(namespace)` and `onlyNamespaceOwner(namespace)` modifiers for access control.
7+
8+
ERC20 module was adapted to use this new version of `WorldConsumer`.

packages/world-consumer/src/examples/SimpleVault.sol

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ interface IERC20 {
1717
/**
1818
* @title SimpleVault (NOT AUDITED)
1919
* @dev Simple example of a Vault that allows accounts with namespace access to transfer its tokens out
20-
* IMPORTANT: this contract expects an existing namespace
2120
*/
2221
contract SimpleVault is WorldConsumer {
2322
error SimpleVault_TransferFailed();
2423

25-
constructor(IBaseWorld world, bytes14 namespace) WorldConsumer(world, namespace, false) {}
24+
bytes14 immutable namespace;
25+
26+
constructor(IBaseWorld world, bytes14 _namespace) WorldConsumer(world) {
27+
namespace = _namespace;
28+
}
2629

2730
// Only accounts with namespace access (e.g. namespace systems) can transfer the ERC20 tokens held by this contract
28-
function transferTo(IERC20 token, address to, uint256 amount) external onlyWorld {
31+
function transferTo(IERC20 token, address to, uint256 amount) external onlyNamespace(namespace) {
2932
require(token.transfer(to, amount), "Transfer failed");
3033
}
3134

packages/world-consumer/src/experimental/WorldConsumer.sol

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,54 @@ import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
66
import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol";
77
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
88
import { ResourceAccess } from "@latticexyz/world/src/codegen/tables/ResourceAccess.sol";
9+
import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol";
910
import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";
1011
import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";
1112
import { WorldContextConsumer } from "@latticexyz/world/src/WorldContext.sol";
1213
import { System } from "@latticexyz/world/src/System.sol";
1314

1415
abstract contract WorldConsumer is System {
15-
bytes14 public immutable namespace;
16-
ResourceId public immutable namespaceId;
17-
1816
error WorldConsumer_RootNamespaceNotAllowed(address worldAddress);
19-
error WorldConsumer_NamespaceAlreadyExists(address worldAddress, bytes14 namespace);
20-
error WorldConsumer_NamespaceDoesNotExists(address worldAddress, bytes14 namespace);
2117
error WorldConsumer_CallerHasNoNamespaceAccess(address worldAddress, bytes14 namespace, address caller);
18+
error WorldConsumer_CallerIsNotNamespaceOwner(address worldAddress, bytes14 namespace, address caller);
2219
error WorldConsumer_CallerIsNotWorld(address worldAddress, address caller);
2320
error WorldConsumer_ValueNotAllowed(address worldAddress);
2421

2522
modifier onlyWorld() {
2623
address world = _world();
27-
if (world != msg.sender) {
28-
revert WorldConsumer_CallerIsNotWorld(world, msg.sender);
29-
}
24+
checkWorldIsCaller(world);
3025
_;
3126
}
3227

33-
modifier onlyNamespace() {
28+
modifier onlyNamespace(bytes14 namespace) {
3429
address world = _world();
35-
if (world != msg.sender) {
36-
revert WorldConsumer_CallerIsNotWorld(world, msg.sender);
37-
}
30+
checkWorldIsCaller(world);
3831

3932
// We use WorldContextConsumer directly as we already know the world is the caller
4033
address sender = WorldContextConsumer._msgSender();
41-
if (!ResourceAccess.get(namespaceId, sender)) {
34+
if (!ResourceAccess.get(WorldResourceIdLib.encodeNamespace(namespace), sender)) {
4235
revert WorldConsumer_CallerHasNoNamespaceAccess(world, namespace, sender);
4336
}
4437

4538
_;
4639
}
4740

48-
constructor(IBaseWorld _world, bytes14 _namespace, bool registerNamespace) {
49-
address worldAddress = address(_world);
50-
StoreSwitch.setStoreAddress(worldAddress);
41+
modifier onlyNamespaceOwner(bytes14 namespace) {
42+
address world = _world();
43+
checkWorldIsCaller(world);
5144

52-
if (_namespace == bytes14(0)) {
53-
revert WorldConsumer_RootNamespaceNotAllowed(worldAddress);
45+
// We use WorldContextConsumer directly as we already know the world is the caller
46+
address sender = WorldContextConsumer._msgSender();
47+
if (NamespaceOwner.get(WorldResourceIdLib.encodeNamespace(namespace)) != sender) {
48+
revert WorldConsumer_CallerIsNotNamespaceOwner(world, namespace, sender);
5449
}
5550

56-
namespace = _namespace;
57-
namespaceId = WorldResourceIdLib.encodeNamespace(_namespace);
58-
bool namespaceExists = ResourceIds.getExists(namespaceId);
51+
_;
52+
}
5953

60-
if (registerNamespace) {
61-
if (namespaceExists) {
62-
revert WorldConsumer_NamespaceAlreadyExists(worldAddress, _namespace);
63-
}
64-
_world.registerNamespace(namespaceId);
65-
} else if (!namespaceExists) {
66-
revert WorldConsumer_NamespaceDoesNotExists(worldAddress, _namespace);
67-
}
54+
constructor(IBaseWorld _world) {
55+
address worldAddress = address(_world);
56+
StoreSwitch.setStoreAddress(worldAddress);
6857
}
6958

7059
function _msgSender() public view virtual override returns (address sender) {
@@ -79,4 +68,10 @@ abstract contract WorldConsumer is System {
7968

8069
return WorldContextConsumer._msgValue();
8170
}
71+
72+
function checkWorldIsCaller(address world) internal view {
73+
if (world != msg.sender) {
74+
revert WorldConsumer_CallerIsNotWorld(world, msg.sender);
75+
}
76+
}
8277
}

packages/world-consumer/test/WorldConsumer.t.sol

Lines changed: 55 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,22 @@ import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
2121
import { WorldConsumer } from "../src/experimental/WorldConsumer.sol";
2222

2323
contract MockWorldConsumer is WorldConsumer {
24-
constructor(
25-
IBaseWorld world,
26-
bytes14 namespace,
27-
bool registerNamespace
28-
) WorldConsumer(world, namespace, registerNamespace) {}
24+
bytes14 immutable namespace;
25+
constructor(IBaseWorld world, bytes14 _namespace) WorldConsumer(world) {
26+
namespace = _namespace;
27+
}
2928

3029
function getStoreAddress() public view virtual returns (address) {
3130
return StoreSwitch.getStoreAddress();
3231
}
3332

34-
function grantNamespaceAccess(address to) external {
35-
IBaseWorld(_world()).grantAccess(namespaceId, to);
36-
}
37-
38-
function transferNamespaceOwnership(address to) external {
39-
IBaseWorld(_world()).transferOwnership(namespaceId, to);
40-
}
41-
4233
function callableByAnyone() external view {}
4334

4435
function onlyCallableByWorld() external view onlyWorld {}
4536

46-
function onlyCallableByNamespace() external view onlyNamespace {}
37+
function onlyCallableByNamespace() external view onlyNamespace(namespace) {}
38+
39+
function onlyCallableByNamespaceOwner() external view onlyNamespaceOwner(namespace) {}
4740

4841
function payableFn() external payable returns (uint256 value) {
4942
return _msgValue();
@@ -53,33 +46,33 @@ contract MockWorldConsumer is WorldConsumer {
5346
contract WorldConsumerTest is Test, GasReporter {
5447
using WorldResourceIdInstance for ResourceId;
5548

56-
function testWorldConsumer() public {
57-
IBaseWorld world = createWorld();
58-
bytes14 namespace = "myNamespace";
59-
ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(namespace);
49+
bytes14 constant namespace = "myNamespace";
50+
bytes16 constant systemName = "mySystem";
6051

61-
MockWorldConsumer mock = new MockWorldConsumer(world, namespace, true);
62-
assertEq(mock.getStoreAddress(), address(world));
63-
assertEq(mock.namespace(), namespace);
64-
assertEq(mock.namespaceId().unwrap(), namespaceId.unwrap());
52+
ResourceId systemId;
53+
ResourceId namespaceId;
54+
55+
IBaseWorld world;
56+
MockWorldConsumer mock;
6557

58+
function setUp() public {
59+
world = createWorld();
6660
StoreSwitch.setStoreAddress(address(world));
6761

68-
assertTrue(ResourceIds.getExists(namespaceId), "Namespace not registered");
62+
namespaceId = WorldResourceIdLib.encodeNamespace(namespace);
63+
world.registerNamespace(namespaceId);
64+
65+
systemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, namespace, systemName);
66+
67+
mock = new MockWorldConsumer(world, namespace);
68+
}
69+
70+
function testWorldConsumer() public view {
71+
assertEq(mock.getStoreAddress(), address(world));
6972
}
7073

7174
// Test internal MUD access control
7275
function testAccessControl() public {
73-
IBaseWorld world = createWorld();
74-
StoreSwitch.setStoreAddress(address(world));
75-
76-
bytes16 systemName = "mySystem";
77-
bytes14 namespace = "myNamespace";
78-
ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(namespace);
79-
ResourceId systemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, namespace, systemName);
80-
MockWorldConsumer mock = new MockWorldConsumer(world, namespace, true);
81-
mock.transferNamespaceOwnership(address(this));
82-
8376
// Register the mock as a system with PRIVATE access
8477
world.registerSystem(systemId, mock, false);
8578

@@ -96,15 +89,6 @@ contract WorldConsumerTest is Test, GasReporter {
9689
}
9790

9891
function testOnlyWorld() public {
99-
IBaseWorld world = createWorld();
100-
StoreSwitch.setStoreAddress(address(world));
101-
102-
bytes16 systemName = "mySystem";
103-
bytes14 namespace = "myNamespace";
104-
ResourceId systemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, namespace, systemName);
105-
MockWorldConsumer mock = new MockWorldConsumer(world, namespace, true);
106-
mock.transferNamespaceOwnership(address(this));
107-
10892
// Register the mock as a system with PUBLIC access
10993
world.registerSystem(systemId, mock, true);
11094

@@ -119,16 +103,6 @@ contract WorldConsumerTest is Test, GasReporter {
119103
}
120104

121105
function testOnlyNamespace() public {
122-
IBaseWorld world = createWorld();
123-
StoreSwitch.setStoreAddress(address(world));
124-
125-
bytes16 systemName = "mySystem";
126-
bytes14 namespace = "myNamespace";
127-
ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(namespace);
128-
ResourceId systemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, namespace, systemName);
129-
MockWorldConsumer mock = new MockWorldConsumer(world, namespace, true);
130-
mock.transferNamespaceOwnership(address(this));
131-
132106
// Register the mock as a system with PUBLIC access
133107
world.registerSystem(systemId, mock, true);
134108

@@ -150,16 +124,37 @@ contract WorldConsumerTest is Test, GasReporter {
150124
world.call(systemId, abi.encodeCall(mock.onlyCallableByNamespace, ()));
151125
}
152126

153-
function testMsgValue() public {
154-
IBaseWorld world = createWorld();
155-
StoreSwitch.setStoreAddress(address(world));
127+
function testOnlyNamespaceOwner() public {
128+
// Register the mock as a system with PUBLIC access
129+
world.registerSystem(systemId, mock, true);
156130

157-
bytes16 systemName = "mySystem";
158-
bytes14 namespace = "myNamespace";
159-
ResourceId systemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, namespace, systemName);
160-
MockWorldConsumer mock = new MockWorldConsumer(world, namespace, true);
161-
mock.transferNamespaceOwnership(address(this));
131+
address alice = address(0x1234);
162132

133+
vm.prank(alice);
134+
vm.expectRevert(abi.encodeWithSelector(WorldConsumer.WorldConsumer_CallerIsNotWorld.selector, world, alice));
135+
mock.onlyCallableByNamespaceOwner();
136+
137+
vm.prank(alice);
138+
vm.expectRevert(
139+
abi.encodeWithSelector(WorldConsumer.WorldConsumer_CallerIsNotNamespaceOwner.selector, world, namespace, alice)
140+
);
141+
world.call(systemId, abi.encodeCall(mock.onlyCallableByNamespaceOwner, ()));
142+
143+
// After granting access to namespace, it should not work
144+
world.grantAccess(namespaceId, alice);
145+
vm.prank(alice);
146+
vm.expectRevert(
147+
abi.encodeWithSelector(WorldConsumer.WorldConsumer_CallerIsNotNamespaceOwner.selector, world, namespace, alice)
148+
);
149+
world.call(systemId, abi.encodeCall(mock.onlyCallableByNamespaceOwner, ()));
150+
151+
// After transfering namespace ownership, it should work
152+
world.transferOwnership(namespaceId, alice);
153+
vm.prank(alice);
154+
world.call(systemId, abi.encodeCall(mock.onlyCallableByNamespaceOwner, ()));
155+
}
156+
157+
function testMsgValue() public {
163158
// Register the mock as a system with PUBLIC access
164159
world.registerSystem(systemId, mock, true);
165160

0 commit comments

Comments
 (0)