From 51914d656d8cd8d851ccc8296d249cf09f53e670 Mon Sep 17 00:00:00 2001
From: alvarius <alvarius@lattice.xyz>
Date: Tue, 12 Sep 2023 22:14:48 +0100
Subject: [PATCH] feat(world): change requireOwnerOrSelf to requireOwner
 (#1457)

---
 .changeset/mighty-eels-type.md                |  16 +++
 .../abi/CoreModule.sol/CoreModule.abi.json    |  23 ++++
 .../CoreModule.sol/CoreModule.abi.json.d.ts   |  23 ++++
 .../world/abi/IModule.sol/IModule.abi.json    |  23 ++++
 .../abi/IModule.sol/IModule.abi.json.d.ts     |  23 ++++
 .../KeysInTableModule.abi.json                |  25 +++-
 .../KeysInTableModule.abi.json.d.ts           |  25 +++-
 .../KeysWithValueModule.abi.json              |  54 ++++-----
 .../KeysWithValueModule.abi.json.d.ts         |  54 ++++-----
 .../StandardDelegationsModule.abi.json        | 108 ++++++++++++++++++
 .../StandardDelegationsModule.abi.json.d.ts   | 108 ++++++++++++++++++
 .../UniqueEntityModule.abi.json               |  23 ++++
 .../UniqueEntityModule.abi.json.d.ts          |  23 ++++
 packages/world/gas-report.json                |  30 ++---
 packages/world/src/AccessControl.sol          |   7 +-
 packages/world/src/World.sol                  |   4 +-
 packages/world/src/interfaces/IModule.sol     |  13 ++-
 .../world/src/modules/core/CoreModule.sol     |  24 ++--
 .../AccessManagementSystem.sol                |   6 +-
 .../StoreRegistrationSystem.sol               |   6 +-
 .../WorldRegistrationSystem.sol               |  10 +-
 .../modules/keysintable/KeysInTableModule.sol |  84 +++++++++++---
 .../keyswithvalue/KeysWithValueModule.sol     |  47 +++++---
 .../StandardDelegationsModule.sol             |  22 +++-
 .../uniqueentity/UniqueEntityModule.sol       |   6 +-
 packages/world/test/KeysInTableModule.t.sol   |  48 ++++----
 packages/world/test/World.t.sol               |  12 +-
 27 files changed, 660 insertions(+), 187 deletions(-)
 create mode 100644 .changeset/mighty-eels-type.md

diff --git a/.changeset/mighty-eels-type.md b/.changeset/mighty-eels-type.md
new file mode 100644
index 0000000000..dd807d4bae
--- /dev/null
+++ b/.changeset/mighty-eels-type.md
@@ -0,0 +1,16 @@
+---
+"@latticexyz/world": major
+---
+
+- The access control library no longer allows calls by the `World` contract to itself to bypass the ownership check.
+  This is a breaking change for root modules that relied on this mechanism to register root tables, systems or function selectors.
+  To upgrade, root modules must use `delegatecall` instead of a regular `call` to install root tables, systems or function selectors.
+
+  ```diff
+  - world.registerSystem(rootSystemId, rootSystemAddress);
+  + address(world).delegatecall(abi.encodeCall(world.registerSystem, (rootSystemId, rootSystemAddress)));
+  ```
+
+- An `installRoot` method was added to the `IModule` interface.
+  This method is now called when installing a root module via `world.installRootModule`.
+  When installing non-root modules via `world.installModule`, the module's `install` function continues to be called.
diff --git a/packages/world/abi/CoreModule.sol/CoreModule.abi.json b/packages/world/abi/CoreModule.sol/CoreModule.abi.json
index 43437a8fc5..472cd505dc 100644
--- a/packages/world/abi/CoreModule.sol/CoreModule.abi.json
+++ b/packages/world/abi/CoreModule.sol/CoreModule.abi.json
@@ -1,4 +1,9 @@
 [
+  {
+    "inputs": [],
+    "name": "NonRootInstallNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -21,6 +26,11 @@
     "name": "RequiredModuleNotFound",
     "type": "error"
   },
+  {
+    "inputs": [],
+    "name": "RootInstallModeNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -145,6 +155,19 @@
     ],
     "name": "install",
     "outputs": [],
+    "stateMutability": "pure",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes",
+        "name": "",
+        "type": "bytes"
+      }
+    ],
+    "name": "installRoot",
+    "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
   }
diff --git a/packages/world/abi/CoreModule.sol/CoreModule.abi.json.d.ts b/packages/world/abi/CoreModule.sol/CoreModule.abi.json.d.ts
index 60ff77131c..1f2c1eb490 100644
--- a/packages/world/abi/CoreModule.sol/CoreModule.abi.json.d.ts
+++ b/packages/world/abi/CoreModule.sol/CoreModule.abi.json.d.ts
@@ -1,4 +1,9 @@
 declare const abi: [
+  {
+    inputs: [];
+    name: "NonRootInstallNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -21,6 +26,11 @@ declare const abi: [
     name: "RequiredModuleNotFound";
     type: "error";
   },
+  {
+    inputs: [];
+    name: "RootInstallModeNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -145,6 +155,19 @@ declare const abi: [
     ];
     name: "install";
     outputs: [];
+    stateMutability: "pure";
+    type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes";
+        name: "";
+        type: "bytes";
+      }
+    ];
+    name: "installRoot";
+    outputs: [];
     stateMutability: "nonpayable";
     type: "function";
   }
diff --git a/packages/world/abi/IModule.sol/IModule.abi.json b/packages/world/abi/IModule.sol/IModule.abi.json
index 0296b60f47..e4683fbb5e 100644
--- a/packages/world/abi/IModule.sol/IModule.abi.json
+++ b/packages/world/abi/IModule.sol/IModule.abi.json
@@ -1,4 +1,9 @@
 [
+  {
+    "inputs": [],
+    "name": "NonRootInstallNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -10,6 +15,11 @@
     "name": "RequiredModuleNotFound",
     "type": "error"
   },
+  {
+    "inputs": [],
+    "name": "RootInstallModeNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [],
     "name": "getName",
@@ -35,5 +45,18 @@
     "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes",
+        "name": "args",
+        "type": "bytes"
+      }
+    ],
+    "name": "installRoot",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
   }
 ]
\ No newline at end of file
diff --git a/packages/world/abi/IModule.sol/IModule.abi.json.d.ts b/packages/world/abi/IModule.sol/IModule.abi.json.d.ts
index d9577d5f19..522a06fb65 100644
--- a/packages/world/abi/IModule.sol/IModule.abi.json.d.ts
+++ b/packages/world/abi/IModule.sol/IModule.abi.json.d.ts
@@ -1,4 +1,9 @@
 declare const abi: [
+  {
+    inputs: [];
+    name: "NonRootInstallNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -10,6 +15,11 @@ declare const abi: [
     name: "RequiredModuleNotFound";
     type: "error";
   },
+  {
+    inputs: [];
+    name: "RootInstallModeNotSupported";
+    type: "error";
+  },
   {
     inputs: [];
     name: "getName";
@@ -35,6 +45,19 @@ declare const abi: [
     outputs: [];
     stateMutability: "nonpayable";
     type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes";
+        name: "args";
+        type: "bytes";
+      }
+    ];
+    name: "installRoot";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
   }
 ];
 export default abi;
diff --git a/packages/world/abi/KeysInTableModule.sol/KeysInTableModule.abi.json b/packages/world/abi/KeysInTableModule.sol/KeysInTableModule.abi.json
index 64373ae108..54e901cb10 100644
--- a/packages/world/abi/KeysInTableModule.sol/KeysInTableModule.abi.json
+++ b/packages/world/abi/KeysInTableModule.sol/KeysInTableModule.abi.json
@@ -1,4 +1,9 @@
 [
+  {
+    "inputs": [],
+    "name": "NonRootInstallNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -10,6 +15,11 @@
     "name": "RequiredModuleNotFound",
     "type": "error"
   },
+  {
+    "inputs": [],
+    "name": "RootInstallModeNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -43,12 +53,25 @@
     "inputs": [
       {
         "internalType": "bytes",
-        "name": "args",
+        "name": "",
         "type": "bytes"
       }
     ],
     "name": "install",
     "outputs": [],
+    "stateMutability": "pure",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes",
+        "name": "args",
+        "type": "bytes"
+      }
+    ],
+    "name": "installRoot",
+    "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
   }
diff --git a/packages/world/abi/KeysInTableModule.sol/KeysInTableModule.abi.json.d.ts b/packages/world/abi/KeysInTableModule.sol/KeysInTableModule.abi.json.d.ts
index 7ceee8b586..96427e2f6b 100644
--- a/packages/world/abi/KeysInTableModule.sol/KeysInTableModule.abi.json.d.ts
+++ b/packages/world/abi/KeysInTableModule.sol/KeysInTableModule.abi.json.d.ts
@@ -1,4 +1,9 @@
 declare const abi: [
+  {
+    inputs: [];
+    name: "NonRootInstallNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -10,6 +15,11 @@ declare const abi: [
     name: "RequiredModuleNotFound";
     type: "error";
   },
+  {
+    inputs: [];
+    name: "RootInstallModeNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -43,12 +53,25 @@ declare const abi: [
     inputs: [
       {
         internalType: "bytes";
-        name: "args";
+        name: "";
         type: "bytes";
       }
     ];
     name: "install";
     outputs: [];
+    stateMutability: "pure";
+    type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes";
+        name: "args";
+        type: "bytes";
+      }
+    ];
+    name: "installRoot";
+    outputs: [];
     stateMutability: "nonpayable";
     type: "function";
   }
diff --git a/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json b/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json
index aeaee69069..54e901cb10 100644
--- a/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json
+++ b/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json
@@ -1,13 +1,7 @@
 [
   {
-    "inputs": [
-      {
-        "internalType": "uint256",
-        "name": "length",
-        "type": "uint256"
-      }
-    ],
-    "name": "PackedCounter_InvalidLength",
+    "inputs": [],
+    "name": "NonRootInstallNotSupported",
     "type": "error"
   },
   {
@@ -21,46 +15,25 @@
     "name": "RequiredModuleNotFound",
     "type": "error"
   },
-  {
-    "inputs": [
-      {
-        "internalType": "uint256",
-        "name": "length",
-        "type": "uint256"
-      }
-    ],
-    "name": "SchemaLib_InvalidLength",
-    "type": "error"
-  },
   {
     "inputs": [],
-    "name": "SchemaLib_StaticTypeAfterDynamicType",
+    "name": "RootInstallModeNotSupported",
     "type": "error"
   },
   {
     "inputs": [
-      {
-        "internalType": "bytes",
-        "name": "data",
-        "type": "bytes"
-      },
-      {
-        "internalType": "uint256",
-        "name": "start",
-        "type": "uint256"
-      },
       {
         "internalType": "uint256",
-        "name": "end",
+        "name": "length",
         "type": "uint256"
       }
     ],
-    "name": "Slice_OutOfBounds",
+    "name": "SchemaLib_InvalidLength",
     "type": "error"
   },
   {
     "inputs": [],
-    "name": "StoreCore_NotDynamicField",
+    "name": "SchemaLib_StaticTypeAfterDynamicType",
     "type": "error"
   },
   {
@@ -80,12 +53,25 @@
     "inputs": [
       {
         "internalType": "bytes",
-        "name": "args",
+        "name": "",
         "type": "bytes"
       }
     ],
     "name": "install",
     "outputs": [],
+    "stateMutability": "pure",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes",
+        "name": "args",
+        "type": "bytes"
+      }
+    ],
+    "name": "installRoot",
+    "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
   }
diff --git a/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json.d.ts b/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json.d.ts
index 4b124c1e24..96427e2f6b 100644
--- a/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json.d.ts
+++ b/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json.d.ts
@@ -1,13 +1,7 @@
 declare const abi: [
   {
-    inputs: [
-      {
-        internalType: "uint256";
-        name: "length";
-        type: "uint256";
-      }
-    ];
-    name: "PackedCounter_InvalidLength";
+    inputs: [];
+    name: "NonRootInstallNotSupported";
     type: "error";
   },
   {
@@ -21,46 +15,25 @@ declare const abi: [
     name: "RequiredModuleNotFound";
     type: "error";
   },
-  {
-    inputs: [
-      {
-        internalType: "uint256";
-        name: "length";
-        type: "uint256";
-      }
-    ];
-    name: "SchemaLib_InvalidLength";
-    type: "error";
-  },
   {
     inputs: [];
-    name: "SchemaLib_StaticTypeAfterDynamicType";
+    name: "RootInstallModeNotSupported";
     type: "error";
   },
   {
     inputs: [
-      {
-        internalType: "bytes";
-        name: "data";
-        type: "bytes";
-      },
-      {
-        internalType: "uint256";
-        name: "start";
-        type: "uint256";
-      },
       {
         internalType: "uint256";
-        name: "end";
+        name: "length";
         type: "uint256";
       }
     ];
-    name: "Slice_OutOfBounds";
+    name: "SchemaLib_InvalidLength";
     type: "error";
   },
   {
     inputs: [];
-    name: "StoreCore_NotDynamicField";
+    name: "SchemaLib_StaticTypeAfterDynamicType";
     type: "error";
   },
   {
@@ -80,12 +53,25 @@ declare const abi: [
     inputs: [
       {
         internalType: "bytes";
-        name: "args";
+        name: "";
         type: "bytes";
       }
     ];
     name: "install";
     outputs: [];
+    stateMutability: "pure";
+    type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes";
+        name: "args";
+        type: "bytes";
+      }
+    ];
+    name: "installRoot";
+    outputs: [];
     stateMutability: "nonpayable";
     type: "function";
   }
diff --git a/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json b/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json
index 646de1964b..1235206c8a 100644
--- a/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json
+++ b/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json
@@ -1,4 +1,9 @@
 [
+  {
+    "inputs": [],
+    "name": "NonRootInstallNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -10,6 +15,11 @@
     "name": "RequiredModuleNotFound",
     "type": "error"
   },
+  {
+    "inputs": [],
+    "name": "RootInstallModeNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -26,6 +36,91 @@
     "name": "SchemaLib_StaticTypeAfterDynamicType",
     "type": "error"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes",
+        "name": "data",
+        "type": "bytes"
+      },
+      {
+        "internalType": "uint256",
+        "name": "start",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "end",
+        "type": "uint256"
+      }
+    ],
+    "name": "Slice_OutOfBounds",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "expected",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "received",
+        "type": "uint256"
+      }
+    ],
+    "name": "StoreCore_InvalidDataLength",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "expected",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "received",
+        "type": "uint256"
+      }
+    ],
+    "name": "StoreCore_InvalidFieldNamesLength",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "expected",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "received",
+        "type": "uint256"
+      }
+    ],
+    "name": "StoreCore_InvalidKeyNamesLength",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes32",
+        "name": "tableId",
+        "type": "bytes32"
+      },
+      {
+        "internalType": "string",
+        "name": "tableIdString",
+        "type": "string"
+      }
+    ],
+    "name": "StoreCore_TableAlreadyExists",
+    "type": "error"
+  },
   {
     "inputs": [],
     "name": "getName",
@@ -49,6 +144,19 @@
     ],
     "name": "install",
     "outputs": [],
+    "stateMutability": "pure",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes",
+        "name": "",
+        "type": "bytes"
+      }
+    ],
+    "name": "installRoot",
+    "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
   }
diff --git a/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json.d.ts b/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json.d.ts
index 3c113d1eed..1b9972cc81 100644
--- a/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json.d.ts
+++ b/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json.d.ts
@@ -1,4 +1,9 @@
 declare const abi: [
+  {
+    inputs: [];
+    name: "NonRootInstallNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -10,6 +15,11 @@ declare const abi: [
     name: "RequiredModuleNotFound";
     type: "error";
   },
+  {
+    inputs: [];
+    name: "RootInstallModeNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -26,6 +36,91 @@ declare const abi: [
     name: "SchemaLib_StaticTypeAfterDynamicType";
     type: "error";
   },
+  {
+    inputs: [
+      {
+        internalType: "bytes";
+        name: "data";
+        type: "bytes";
+      },
+      {
+        internalType: "uint256";
+        name: "start";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "end";
+        type: "uint256";
+      }
+    ];
+    name: "Slice_OutOfBounds";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "expected";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "received";
+        type: "uint256";
+      }
+    ];
+    name: "StoreCore_InvalidDataLength";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "expected";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "received";
+        type: "uint256";
+      }
+    ];
+    name: "StoreCore_InvalidFieldNamesLength";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "expected";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "received";
+        type: "uint256";
+      }
+    ];
+    name: "StoreCore_InvalidKeyNamesLength";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32";
+        name: "tableId";
+        type: "bytes32";
+      },
+      {
+        internalType: "string";
+        name: "tableIdString";
+        type: "string";
+      }
+    ];
+    name: "StoreCore_TableAlreadyExists";
+    type: "error";
+  },
   {
     inputs: [];
     name: "getName";
@@ -49,6 +144,19 @@ declare const abi: [
     ];
     name: "install";
     outputs: [];
+    stateMutability: "pure";
+    type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes";
+        name: "";
+        type: "bytes";
+      }
+    ];
+    name: "installRoot";
+    outputs: [];
     stateMutability: "nonpayable";
     type: "function";
   }
diff --git a/packages/world/abi/UniqueEntityModule.sol/UniqueEntityModule.abi.json b/packages/world/abi/UniqueEntityModule.sol/UniqueEntityModule.abi.json
index 646de1964b..90b64315e3 100644
--- a/packages/world/abi/UniqueEntityModule.sol/UniqueEntityModule.abi.json
+++ b/packages/world/abi/UniqueEntityModule.sol/UniqueEntityModule.abi.json
@@ -1,4 +1,9 @@
 [
+  {
+    "inputs": [],
+    "name": "NonRootInstallNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -10,6 +15,11 @@
     "name": "RequiredModuleNotFound",
     "type": "error"
   },
+  {
+    "inputs": [],
+    "name": "RootInstallModeNotSupported",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -51,5 +61,18 @@
     "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes",
+        "name": "args",
+        "type": "bytes"
+      }
+    ],
+    "name": "installRoot",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
   }
 ]
\ No newline at end of file
diff --git a/packages/world/abi/UniqueEntityModule.sol/UniqueEntityModule.abi.json.d.ts b/packages/world/abi/UniqueEntityModule.sol/UniqueEntityModule.abi.json.d.ts
index 3c113d1eed..7965ef95be 100644
--- a/packages/world/abi/UniqueEntityModule.sol/UniqueEntityModule.abi.json.d.ts
+++ b/packages/world/abi/UniqueEntityModule.sol/UniqueEntityModule.abi.json.d.ts
@@ -1,4 +1,9 @@
 declare const abi: [
+  {
+    inputs: [];
+    name: "NonRootInstallNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -10,6 +15,11 @@ declare const abi: [
     name: "RequiredModuleNotFound";
     type: "error";
   },
+  {
+    inputs: [];
+    name: "RootInstallModeNotSupported";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -51,6 +61,19 @@ declare const abi: [
     outputs: [];
     stateMutability: "nonpayable";
     type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes";
+        name: "args";
+        type: "bytes";
+      }
+    ];
+    name: "installRoot";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
   }
 ];
 export default abi;
diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json
index 0029f237db..980f15a975 100644
--- a/packages/world/gas-report.json
+++ b/packages/world/gas-report.json
@@ -39,13 +39,13 @@
     "file": "test/KeysInTableModule.t.sol",
     "test": "testInstallComposite",
     "name": "install keys in table module",
-    "gasUsed": 1413349
+    "gasUsed": 1433865
   },
   {
     "file": "test/KeysInTableModule.t.sol",
     "test": "testInstallGas",
     "name": "install keys in table module",
-    "gasUsed": 1413349
+    "gasUsed": 1433865
   },
   {
     "file": "test/KeysInTableModule.t.sol",
@@ -57,13 +57,13 @@
     "file": "test/KeysInTableModule.t.sol",
     "test": "testInstallSingleton",
     "name": "install keys in table module",
-    "gasUsed": 1413349
+    "gasUsed": 1433865
   },
   {
     "file": "test/KeysInTableModule.t.sol",
     "test": "testSetAndDeleteRecordHookCompositeGas",
     "name": "install keys in table module",
-    "gasUsed": 1413349
+    "gasUsed": 1433865
   },
   {
     "file": "test/KeysInTableModule.t.sol",
@@ -81,7 +81,7 @@
     "file": "test/KeysInTableModule.t.sol",
     "test": "testSetAndDeleteRecordHookGas",
     "name": "install keys in table module",
-    "gasUsed": 1413349
+    "gasUsed": 1433865
   },
   {
     "file": "test/KeysInTableModule.t.sol",
@@ -99,7 +99,7 @@
     "file": "test/KeysWithValueModule.t.sol",
     "test": "testGetKeysWithValueGas",
     "name": "install keys with value module",
-    "gasUsed": 651540
+    "gasUsed": 677067
   },
   {
     "file": "test/KeysWithValueModule.t.sol",
@@ -117,7 +117,7 @@
     "file": "test/KeysWithValueModule.t.sol",
     "test": "testInstall",
     "name": "install keys with value module",
-    "gasUsed": 651540
+    "gasUsed": 677067
   },
   {
     "file": "test/KeysWithValueModule.t.sol",
@@ -129,7 +129,7 @@
     "file": "test/KeysWithValueModule.t.sol",
     "test": "testSetAndDeleteRecordHook",
     "name": "install keys with value module",
-    "gasUsed": 651540
+    "gasUsed": 677067
   },
   {
     "file": "test/KeysWithValueModule.t.sol",
@@ -147,7 +147,7 @@
     "file": "test/KeysWithValueModule.t.sol",
     "test": "testSetField",
     "name": "install keys with value module",
-    "gasUsed": 651540
+    "gasUsed": 677067
   },
   {
     "file": "test/KeysWithValueModule.t.sol",
@@ -255,7 +255,7 @@
     "file": "test/UniqueEntityModule.t.sol",
     "test": "testInstall",
     "name": "install unique entity module",
-    "gasUsed": 727309
+    "gasUsed": 727195
   },
   {
     "file": "test/UniqueEntityModule.t.sol",
@@ -267,7 +267,7 @@
     "file": "test/UniqueEntityModule.t.sol",
     "test": "testInstallRoot",
     "name": "installRoot unique entity module",
-    "gasUsed": 706087
+    "gasUsed": 712496
   },
   {
     "file": "test/UniqueEntityModule.t.sol",
@@ -309,19 +309,19 @@
     "file": "test/World.t.sol",
     "test": "testRegisterFallbackSystem",
     "name": "Register a fallback system",
-    "gasUsed": 70620
+    "gasUsed": 70563
   },
   {
     "file": "test/World.t.sol",
     "test": "testRegisterFallbackSystem",
     "name": "Register a root fallback system",
-    "gasUsed": 63860
+    "gasUsed": 63803
   },
   {
     "file": "test/World.t.sol",
     "test": "testRegisterFunctionSelector",
     "name": "Register a function selector",
-    "gasUsed": 91214
+    "gasUsed": 91157
   },
   {
     "file": "test/World.t.sol",
@@ -333,7 +333,7 @@
     "file": "test/World.t.sol",
     "test": "testRegisterRootFunctionSelector",
     "name": "Register a root function selector",
-    "gasUsed": 79771
+    "gasUsed": 85721
   },
   {
     "file": "test/World.t.sol",
diff --git a/packages/world/src/AccessControl.sol b/packages/world/src/AccessControl.sol
index ae42b34b02..ca637f7c3f 100644
--- a/packages/world/src/AccessControl.sol
+++ b/packages/world/src/AccessControl.sol
@@ -32,12 +32,11 @@ library AccessControl {
   }
 
   /**
-   * Check for ownership of the namespace of the given resource selector
-   * or identity of the caller to this contract's own address.
+   * Check for ownership of the namespace of the given resource selector.
    * Reverts with AccessDenied if the check fails.
    */
-  function requireOwnerOrSelf(bytes32 resourceSelector, address caller) internal view {
-    if (address(this) != caller && NamespaceOwner.get(resourceSelector.getNamespace()) != caller) {
+  function requireOwner(bytes32 resourceSelector, address caller) internal view {
+    if (NamespaceOwner.get(resourceSelector.getNamespace()) != caller) {
       revert IWorldErrors.AccessDenied(resourceSelector.toString(), caller);
     }
   }
diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol
index 8f5256ef75..75839ca583 100644
--- a/packages/world/src/World.sol
+++ b/packages/world/src/World.sol
@@ -49,13 +49,13 @@ contract World is StoreRead, IStoreData, IWorldKernel {
    * The module is delegatecalled and installed in the root namespace.
    */
   function installRootModule(IModule module, bytes memory args) public {
-    AccessControl.requireOwnerOrSelf(ROOT_NAMESPACE, msg.sender);
+    AccessControl.requireOwner(ROOT_NAMESPACE, msg.sender);
 
     WorldContextProvider.delegatecallWithContextOrRevert({
       msgSender: msg.sender,
       msgValue: 0,
       target: address(module),
-      funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, args)
+      funcSelectorAndArgs: abi.encodeWithSelector(IModule.installRoot.selector, args)
     });
 
     // Register the module in the InstalledModules table
diff --git a/packages/world/src/interfaces/IModule.sol b/packages/world/src/interfaces/IModule.sol
index 5e9dda86ed..990fbcde48 100644
--- a/packages/world/src/interfaces/IModule.sol
+++ b/packages/world/src/interfaces/IModule.sol
@@ -3,6 +3,8 @@ pragma solidity >=0.8.0;
 
 interface IModule {
   error RequiredModuleNotFound(string resourceSelector);
+  error RootInstallModeNotSupported();
+  error NonRootInstallNotSupported();
 
   /**
    * Return the module name as a bytes16.
@@ -10,7 +12,16 @@ interface IModule {
   function getName() external view returns (bytes16 name);
 
   /**
-   * A module expects to be called via the World contract, and therefore installs itself on its `msg.sender`.
+   * This function is called by the World as part of `installRootModule`.
+   * The module expects to be called via the World contract, and therefore installs itself on the `msg.sender`.
+   */
+  function installRoot(bytes memory args) external;
+
+  /**
+   * This function is called by the World as part of `installModule`.
+   * The module expects to be called via the World contract, and therefore installs itself on the `msg.sender`.
+   * This function is separate from `installRoot` because the logic might be different (eg. accepting namespace parameters,
+   * or using `callFrom`)
    */
   function install(bytes memory args) external;
 }
diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol
index 72842d9c84..2a2e44dc7b 100644
--- a/packages/world/src/modules/core/CoreModule.sol
+++ b/packages/world/src/modules/core/CoreModule.sol
@@ -37,7 +37,7 @@ import { WorldRegistrationSystem } from "./implementations/WorldRegistrationSyst
  * The CoreModule registers internal World tables, the CoreSystem, and its function selectors.
 
  * Note:
- * This module is required to be delegatecalled (via `World.registerRootSystem`),
+ * This module only supports `installRoot` (via `World.registerRootSystem`),
  * because it needs to install root tables, systems and function selectors.
  */
 contract CoreModule is IModule, WorldContextConsumer {
@@ -49,12 +49,16 @@ contract CoreModule is IModule, WorldContextConsumer {
     return CORE_MODULE_NAME;
   }
 
-  function install(bytes memory) public override {
+  function installRoot(bytes memory) public override {
     _registerCoreTables();
     _registerCoreSystem();
     _registerFunctionSelectors();
   }
 
+  function install(bytes memory) public pure {
+    revert NonRootInstallNotSupported();
+  }
+
   /**
    * Register core tables in the World
    */
@@ -81,11 +85,9 @@ contract CoreModule is IModule, WorldContextConsumer {
       msgSender: _msgSender(),
       msgValue: 0,
       target: coreSystem,
-      funcSelectorAndArgs: abi.encodeWithSelector(
-        WorldRegistrationSystem.registerSystem.selector,
-        ResourceSelector.from(ROOT_NAMESPACE, CORE_SYSTEM_NAME),
-        coreSystem,
-        true
+      funcSelectorAndArgs: abi.encodeCall(
+        WorldRegistrationSystem.registerSystem,
+        (ResourceSelector.from(ROOT_NAMESPACE, CORE_SYSTEM_NAME), CoreSystem(coreSystem), true)
       )
     });
   }
@@ -127,11 +129,9 @@ contract CoreModule is IModule, WorldContextConsumer {
         msgSender: _msgSender(),
         msgValue: 0,
         target: coreSystem,
-        funcSelectorAndArgs: abi.encodeWithSelector(
-          WorldRegistrationSystem.registerRootFunctionSelector.selector,
-          ResourceSelector.from(ROOT_NAMESPACE, CORE_SYSTEM_NAME),
-          functionSelectors[i],
-          functionSelectors[i]
+        funcSelectorAndArgs: abi.encodeCall(
+          WorldRegistrationSystem.registerRootFunctionSelector,
+          (ResourceSelector.from(ROOT_NAMESPACE, CORE_SYSTEM_NAME), functionSelectors[i], functionSelectors[i])
         )
       });
     }
diff --git a/packages/world/src/modules/core/implementations/AccessManagementSystem.sol b/packages/world/src/modules/core/implementations/AccessManagementSystem.sol
index d85a759f84..c29d693ac0 100644
--- a/packages/world/src/modules/core/implementations/AccessManagementSystem.sol
+++ b/packages/world/src/modules/core/implementations/AccessManagementSystem.sol
@@ -19,7 +19,7 @@ contract AccessManagementSystem is System {
    */
   function grantAccess(bytes32 resourceSelector, address grantee) public virtual {
     // Require the caller to own the namespace
-    AccessControl.requireOwnerOrSelf(resourceSelector, _msgSender());
+    AccessControl.requireOwner(resourceSelector, _msgSender());
 
     // Grant access to the given resource
     ResourceAccess.set(resourceSelector, grantee, true);
@@ -31,7 +31,7 @@ contract AccessManagementSystem is System {
    */
   function revokeAccess(bytes32 resourceSelector, address grantee) public virtual {
     // Require the caller to own the namespace
-    AccessControl.requireOwnerOrSelf(resourceSelector, _msgSender());
+    AccessControl.requireOwner(resourceSelector, _msgSender());
 
     // Revoke access from the given resource
     ResourceAccess.deleteRecord(resourceSelector, grantee);
@@ -44,7 +44,7 @@ contract AccessManagementSystem is System {
    */
   function transferOwnership(bytes16 namespace, address newOwner) public virtual {
     // Require the caller to own the namespace
-    AccessControl.requireOwnerOrSelf(namespace, _msgSender());
+    AccessControl.requireOwner(namespace, _msgSender());
 
     // Set namespace new owner
     NamespaceOwner.set(namespace, newOwner);
diff --git a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol
index 4d9c77bc00..bf6a081237 100644
--- a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol
+++ b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol
@@ -59,7 +59,7 @@ contract StoreRegistrationSystem is System, IWorldErrors {
       });
     } else {
       // otherwise require caller to own the namespace
-      AccessControl.requireOwnerOrSelf(namespace, _msgSender());
+      AccessControl.requireOwner(namespace, _msgSender());
     }
 
     // Require no resource to exist at this selector yet
@@ -80,7 +80,7 @@ contract StoreRegistrationSystem is System, IWorldErrors {
    */
   function registerStoreHook(bytes32 tableId, IStoreHook hookAddress, uint8 enabledHooksBitmap) public virtual {
     // Require caller to own the namespace
-    AccessControl.requireOwnerOrSelf(tableId, _msgSender());
+    AccessControl.requireOwner(tableId, _msgSender());
 
     // Register the hook
     StoreCore.registerStoreHook(tableId, hookAddress, enabledHooksBitmap);
@@ -92,7 +92,7 @@ contract StoreRegistrationSystem is System, IWorldErrors {
    */
   function unregisterStoreHook(bytes32 tableId, IStoreHook hookAddress) public virtual {
     // Require caller to own the namespace
-    AccessControl.requireOwnerOrSelf(tableId, _msgSender());
+    AccessControl.requireOwner(tableId, _msgSender());
 
     // Unregister the hook
     StoreCore.unregisterStoreHook(tableId, hookAddress);
diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol
index e0153c7795..c64337b454 100644
--- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol
+++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol
@@ -58,7 +58,7 @@ contract WorldRegistrationSystem is System, IWorldErrors {
     uint8 enabledHooksBitmap
   ) public virtual {
     // Require caller to own the namespace
-    AccessControl.requireOwnerOrSelf(resourceSelector, _msgSender());
+    AccessControl.requireOwner(resourceSelector, _msgSender());
 
     // Register the hook
     SystemHooks.push(resourceSelector, Hook.unwrap(SystemHookLib.encode(hookAddress, enabledHooksBitmap)));
@@ -69,7 +69,7 @@ contract WorldRegistrationSystem is System, IWorldErrors {
    */
   function unregisterSystemHook(bytes32 resourceSelector, ISystemHook hookAddress) public virtual {
     // Require caller to own the namespace
-    AccessControl.requireOwnerOrSelf(resourceSelector, _msgSender());
+    AccessControl.requireOwner(resourceSelector, _msgSender());
 
     // Remove the hook from the list of hooks for this resourceSelector in the system hooks table
     HookLib.filterListByAddress(SystemHooksTableId, resourceSelector, address(hookAddress));
@@ -98,7 +98,7 @@ contract WorldRegistrationSystem is System, IWorldErrors {
     // otherwise require caller to own the namespace
     bytes16 namespace = resourceSelector.getNamespace();
     if (ResourceType.get(namespace) == Resource.NONE) registerNamespace(namespace);
-    else AccessControl.requireOwnerOrSelf(namespace, _msgSender());
+    else AccessControl.requireOwner(namespace, _msgSender());
 
     // Require no resource other than a system to exist at this selector yet
     Resource resourceType = ResourceType.get(resourceSelector);
@@ -144,7 +144,7 @@ contract WorldRegistrationSystem is System, IWorldErrors {
     string memory systemFunctionArguments
   ) public returns (bytes4 worldFunctionSelector) {
     // Require the caller to own the namespace
-    AccessControl.requireOwnerOrSelf(resourceSelector, _msgSender());
+    AccessControl.requireOwner(resourceSelector, _msgSender());
 
     // Compute global function selector
     string memory namespaceString = ResourceSelector.toTrimmedString(resourceSelector.getNamespace());
@@ -179,7 +179,7 @@ contract WorldRegistrationSystem is System, IWorldErrors {
     bytes4 systemFunctionSelector
   ) public returns (bytes4) {
     // Require the caller to own the root namespace
-    AccessControl.requireOwnerOrSelf(ROOT_NAMESPACE, _msgSender());
+    AccessControl.requireOwner(ROOT_NAMESPACE, _msgSender());
 
     // Require the function selector to be globally unique
     bytes32 existingResourceSelector = FunctionSelectors.getResourceSelector(worldFunctionSelector);
diff --git a/packages/world/src/modules/keysintable/KeysInTableModule.sol b/packages/world/src/modules/keysintable/KeysInTableModule.sol
index 253d9d0f9f..b01cf1ea49 100644
--- a/packages/world/src/modules/keysintable/KeysInTableModule.sol
+++ b/packages/world/src/modules/keysintable/KeysInTableModule.sol
@@ -11,6 +11,7 @@ import { IModule } from "../../interfaces/IModule.sol";
 
 import { WorldContextConsumer } from "../../WorldContext.sol";
 import { ResourceSelector } from "../../ResourceSelector.sol";
+import { revertWithBytes } from "../../revertWithBytes.sol";
 
 import { KeysInTableHook } from "./KeysInTableHook.sol";
 import { KeysInTable, KeysInTableTableId } from "./tables/KeysInTable.sol";
@@ -23,48 +24,93 @@ import { UsedKeysIndex, UsedKeysIndexTableId } from "./tables/UsedKeysIndex.sol"
  *
  * Note: if a table with composite keys is used, only the first key is indexed
  *
- * Note: this module currently expects to be `delegatecalled` via World.installRootModule.
- * Support for installing it via `World.installModule` depends on `World.callFrom` being implemented.
+ * Note: this module currently only supports `installRoot` (via `World.installRootModule`).
+ * TODO: add support for `install` (via `World.installModule`) by using `callFrom` with the `msgSender()`
  */
 contract KeysInTableModule is IModule, WorldContextConsumer {
   using ResourceSelector for bytes32;
 
   // The KeysInTableHook is deployed once and infers the target table id
   // from the source table id (passed as argument to the hook methods)
-  KeysInTableHook immutable hook = new KeysInTableHook();
+  KeysInTableHook private immutable hook = new KeysInTableHook();
 
   function getName() public pure returns (bytes16) {
     return bytes16("keysInTable");
   }
 
-  function install(bytes memory args) public override {
+  function installRoot(bytes memory args) public override {
     // Extract source table id from args
     bytes32 sourceTableId = abi.decode(args, (bytes32));
 
     IBaseWorld world = IBaseWorld(_world());
 
+    // Initialize variable to reuse in low level calls
+    bool success;
+    bytes memory returnData;
+
     if (ResourceType.get(KeysInTableTableId) == Resource.NONE) {
       // Register the tables
-      KeysInTable.register(world);
-      UsedKeysIndex.register(world);
+      (success, returnData) = address(world).delegatecall(
+        abi.encodeCall(
+          world.registerTable,
+          (
+            KeysInTableTableId,
+            KeysInTable.getKeySchema(),
+            KeysInTable.getValueSchema(),
+            KeysInTable.getKeyNames(),
+            KeysInTable.getFieldNames()
+          )
+        )
+      );
+      if (!success) revertWithBytes(returnData);
+
+      (success, returnData) = address(world).delegatecall(
+        abi.encodeCall(
+          world.registerTable,
+          (
+            UsedKeysIndexTableId,
+            UsedKeysIndex.getKeySchema(),
+            UsedKeysIndex.getValueSchema(),
+            UsedKeysIndex.getKeyNames(),
+            UsedKeysIndex.getFieldNames()
+          )
+        )
+      );
+      if (!success) revertWithBytes(returnData);
 
       // Grant the hook access to the tables
-      world.grantAccess(KeysInTableTableId, address(hook));
-      world.grantAccess(UsedKeysIndexTableId, address(hook));
+      (success, returnData) = address(world).delegatecall(
+        abi.encodeCall(world.grantAccess, (KeysInTableTableId, address(hook)))
+      );
+      if (!success) revertWithBytes(returnData);
+
+      (success, returnData) = address(world).delegatecall(
+        abi.encodeCall(world.grantAccess, (UsedKeysIndexTableId, address(hook)))
+      );
+      if (!success) revertWithBytes(returnData);
     }
 
     // Register a hook that is called when a value is set in the source table
-    world.registerStoreHook(
-      sourceTableId,
-      hook,
-      StoreHookLib.encodeBitmap({
-        onBeforeSetRecord: true,
-        onAfterSetRecord: false,
-        onBeforeSetField: false,
-        onAfterSetField: true,
-        onBeforeDeleteRecord: true,
-        onAfterDeleteRecord: false
-      })
+    (success, returnData) = address(world).delegatecall(
+      abi.encodeCall(
+        world.registerStoreHook,
+        (
+          sourceTableId,
+          hook,
+          StoreHookLib.encodeBitmap({
+            onBeforeSetRecord: true,
+            onAfterSetRecord: false,
+            onBeforeSetField: false,
+            onAfterSetField: true,
+            onBeforeDeleteRecord: true,
+            onAfterDeleteRecord: false
+          })
+        )
+      )
     );
   }
+
+  function install(bytes memory) public pure {
+    revert NonRootInstallNotSupported();
+  }
 }
diff --git a/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol b/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol
index 1cd17800d2..dad35bd968 100644
--- a/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol
+++ b/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol
@@ -9,6 +9,7 @@ import { IModule } from "../../interfaces/IModule.sol";
 
 import { WorldContextConsumer } from "../../WorldContext.sol";
 import { ResourceSelector } from "../../ResourceSelector.sol";
+import { revertWithBytes } from "../../revertWithBytes.sol";
 
 import { MODULE_NAMESPACE } from "./constants.sol";
 import { KeysWithValueHook } from "./KeysWithValueHook.sol";
@@ -23,43 +24,55 @@ import { getTargetTableSelector } from "../utils/getTargetTableSelector.sol";
  *
  * Note: if a table with composite keys is used, only the first key is indexed
  *
- * Note: this module currently expects to be `delegatecalled` via World.installRootModule.
- * Support for installing it via `World.installModule` depends on `World.callFrom` being implemented.
+ * Note: this module currently only supports `installRoot` (via `World.installRootModule`).
+ * TODO: add support for `install` (via `World.installModule`) by using `callFrom` with the `msgSender()`
  */
 contract KeysWithValueModule is IModule, WorldContextConsumer {
   using ResourceSelector for bytes32;
 
   // The KeysWithValueHook is deployed once and infers the target table id
   // from the source table id (passed as argument to the hook methods)
-  KeysWithValueHook immutable hook = new KeysWithValueHook();
+  KeysWithValueHook private immutable hook = new KeysWithValueHook();
 
   function getName() public pure returns (bytes16) {
     return bytes16("index");
   }
 
-  function install(bytes memory args) public override {
+  function installRoot(bytes memory args) public {
     // Extract source table id from args
     bytes32 sourceTableId = abi.decode(args, (bytes32));
     bytes32 targetTableSelector = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId);
 
+    IBaseWorld world = IBaseWorld(_world());
+
     // Register the target table
-    KeysWithValue.register(IBaseWorld(_world()), targetTableSelector);
+    KeysWithValue.register(world, targetTableSelector);
 
     // Grant the hook access to the target table
-    IBaseWorld(_world()).grantAccess(targetTableSelector, address(hook));
+    world.grantAccess(targetTableSelector, address(hook));
 
     // Register a hook that is called when a value is set in the source table
-    StoreSwitch.registerStoreHook(
-      sourceTableId,
-      hook,
-      StoreHookLib.encodeBitmap({
-        onBeforeSetRecord: true,
-        onAfterSetRecord: false,
-        onBeforeSetField: true,
-        onAfterSetField: true,
-        onBeforeDeleteRecord: true,
-        onAfterDeleteRecord: false
-      })
+    (bool success, bytes memory returnData) = address(world).delegatecall(
+      abi.encodeCall(
+        world.registerStoreHook,
+        (
+          sourceTableId,
+          hook,
+          StoreHookLib.encodeBitmap({
+            onBeforeSetRecord: true,
+            onAfterSetRecord: false,
+            onBeforeSetField: true,
+            onAfterSetField: true,
+            onBeforeDeleteRecord: true,
+            onAfterDeleteRecord: false
+          })
+        )
+      )
     );
+    if (!success) revertWithBytes(returnData);
+  }
+
+  function install(bytes memory) public pure {
+    revert NonRootInstallNotSupported();
   }
 }
diff --git a/packages/world/src/modules/std-delegations/StandardDelegationsModule.sol b/packages/world/src/modules/std-delegations/StandardDelegationsModule.sol
index 97d31ef2ec..74057574dc 100644
--- a/packages/world/src/modules/std-delegations/StandardDelegationsModule.sol
+++ b/packages/world/src/modules/std-delegations/StandardDelegationsModule.sol
@@ -6,6 +6,7 @@ import { IModule } from "../../interfaces/IModule.sol";
 
 import { WorldContextConsumer } from "../../WorldContext.sol";
 import { ResourceSelector } from "../../ResourceSelector.sol";
+import { revertWithBytes } from "../../revertWithBytes.sol";
 
 import { CallboundDelegationControl } from "./CallboundDelegationControl.sol";
 import { TimeboundDelegationControl } from "./TimeboundDelegationControl.sol";
@@ -25,15 +26,26 @@ contract StandardDelegationsModule is IModule, WorldContextConsumer {
     return MODULE_NAME;
   }
 
-  function install(bytes memory) public {
+  function installRoot(bytes memory) public {
     IBaseWorld world = IBaseWorld(_world());
 
     // Register tables
-    CallboundDelegations.register(world);
-    TimeboundDelegations.register(world);
+    CallboundDelegations.register();
+    TimeboundDelegations.register();
 
     // Register systems
-    world.registerSystem(CALLBOUND_DELEGATION, callboundDelegationControl, true);
-    world.registerSystem(TIMEBOUND_DELEGATION, timeboundDelegationControl, true);
+    (bool success, bytes memory returnData) = address(world).delegatecall(
+      abi.encodeCall(world.registerSystem, (CALLBOUND_DELEGATION, callboundDelegationControl, true))
+    );
+    if (!success) revertWithBytes(returnData);
+
+    (success, returnData) = address(world).delegatecall(
+      abi.encodeCall(world.registerSystem, (TIMEBOUND_DELEGATION, timeboundDelegationControl, true))
+    );
+    if (!success) revertWithBytes(returnData);
+  }
+
+  function install(bytes memory) public pure {
+    revert NonRootInstallNotSupported();
   }
 }
diff --git a/packages/world/src/modules/uniqueentity/UniqueEntityModule.sol b/packages/world/src/modules/uniqueentity/UniqueEntityModule.sol
index cfff61f6bc..18d87c7dfe 100644
--- a/packages/world/src/modules/uniqueentity/UniqueEntityModule.sol
+++ b/packages/world/src/modules/uniqueentity/UniqueEntityModule.sol
@@ -19,12 +19,16 @@ import { NAMESPACE, MODULE_NAME, SYSTEM_NAME, TABLE_NAME } from "./constants.sol
 contract UniqueEntityModule is IModule, WorldContextConsumer {
   // Since the UniqueEntitySystem only exists once per World and writes to
   // known tables, we can deploy it once and register it in multiple Worlds.
-  UniqueEntitySystem immutable uniqueEntitySystem = new UniqueEntitySystem();
+  UniqueEntitySystem private immutable uniqueEntitySystem = new UniqueEntitySystem();
 
   function getName() public pure returns (bytes16) {
     return MODULE_NAME;
   }
 
+  function installRoot(bytes memory args) public {
+    install(args);
+  }
+
   function install(bytes memory) public {
     IBaseWorld world = IBaseWorld(_world());
 
diff --git a/packages/world/test/KeysInTableModule.t.sol b/packages/world/test/KeysInTableModule.t.sol
index 1968d14a00..3446b8e238 100644
--- a/packages/world/test/KeysInTableModule.t.sol
+++ b/packages/world/test/KeysInTableModule.t.sol
@@ -20,30 +20,30 @@ import { hasKey } from "../src/modules/keysintable/hasKey.sol";
 
 contract KeysInTableModuleTest is Test, GasReporter {
   using ResourceSelector for bytes32;
-  IBaseWorld world;
-  KeysInTableModule keysInTableModule = new KeysInTableModule(); // Modules can be deployed once and installed multiple times
-
-  bytes16 namespace = ROOT_NAMESPACE;
-  bytes16 name = bytes16("source");
-  bytes16 singletonName = bytes16("singleton");
-  bytes16 compositeName = bytes16("composite");
-  bytes32 key1 = keccak256("test");
-  bytes32[] keyTuple1;
-  bytes32 key2 = keccak256("test2");
-  bytes32[] keyTuple2;
-  bytes32 key3 = keccak256("test3");
-  bytes32[] keyTuple3;
-
-  Schema tableValueSchema;
-  Schema tableKeySchema;
-  Schema singletonKeySchema;
-  Schema compositeKeySchema;
-  bytes32 tableId = ResourceSelector.from(namespace, name);
-  bytes32 singletonTableId = ResourceSelector.from(namespace, singletonName);
-  bytes32 compositeTableId = ResourceSelector.from(namespace, compositeName);
-
-  uint256 val1 = 123;
-  uint256 val2 = 42;
+  IBaseWorld private world;
+  KeysInTableModule private keysInTableModule = new KeysInTableModule(); // Modules can be deployed once and installed multiple times
+
+  bytes16 private namespace = ROOT_NAMESPACE;
+  bytes16 private name = bytes16("source");
+  bytes16 private singletonName = bytes16("singleton");
+  bytes16 private compositeName = bytes16("composite");
+  bytes32 private key1 = keccak256("test");
+  bytes32[] private keyTuple1;
+  bytes32 private key2 = keccak256("test2");
+  bytes32[] private keyTuple2;
+  bytes32 private key3 = keccak256("test3");
+  bytes32[] private keyTuple3;
+
+  Schema private tableValueSchema;
+  Schema private tableKeySchema;
+  Schema private singletonKeySchema;
+  Schema private compositeKeySchema;
+  bytes32 private tableId = ResourceSelector.from(namespace, name);
+  bytes32 private singletonTableId = ResourceSelector.from(namespace, singletonName);
+  bytes32 private compositeTableId = ResourceSelector.from(namespace, compositeName);
+
+  uint256 private val1 = 123;
+  uint256 private val2 = 42;
 
   function setUp() public {
     tableValueSchema = SchemaEncodeHelper.encode(SchemaType.UINT256);
diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol
index 1be65a4342..bfe8ee79c5 100644
--- a/packages/world/test/World.t.sol
+++ b/packages/world/test/World.t.sol
@@ -304,8 +304,8 @@ contract WorldTest is Test, GasReporter {
     _expectAccessDenied(address(0x01), namespace, "");
     world.registerTable(otherTableSelector, defaultKeySchema, valueSchema, keyNames, fieldNames);
 
-    // Expect the World to be allowed to call registerTable
-    vm.prank(address(world));
+    // Expect the World to not be allowed to call registerTable via an external call
+    _expectAccessDenied(address(world), namespace, "");
     world.registerTable(otherTableSelector, defaultKeySchema, valueSchema, keyNames, fieldNames);
   }
 
@@ -362,8 +362,8 @@ contract WorldTest is Test, GasReporter {
     _expectAccessDenied(address(0x01), "", "");
     world.registerSystem(ResourceSelector.from("", "rootSystem"), yetAnotherSystem, true);
 
-    // Expect the registration to succeed when coming from the World
-    vm.prank(address(world));
+    // Expect the registration to fail when coming from the World (since the World address doesn't have access)
+    _expectAccessDenied(address(world), "", "");
     world.registerSystem(ResourceSelector.from("", "rootSystem"), yetAnotherSystem, true);
   }
 
@@ -1047,8 +1047,8 @@ contract WorldTest is Test, GasReporter {
     _expectAccessDenied(address(0x01), "", "");
     world.registerRootFunctionSelector(resourceSelector, worldFunc, sysFunc);
 
-    // Expect the World to be able to register a root function selector
-    vm.prank(address(world));
+    // Expect the World to not be able to register a root function selector when calling the function externally
+    _expectAccessDenied(address(world), "", "");
     world.registerRootFunctionSelector(resourceSelector, "smth", "smth");
 
     startGasReport("Register a root function selector");