diff --git a/.changeset/pink-tips-give.md b/.changeset/pink-tips-give.md
new file mode 100644
index 0000000000..cff922f8d4
--- /dev/null
+++ b/.changeset/pink-tips-give.md
@@ -0,0 +1,22 @@
+---
+"@latticexyz/world": major
+---
+
+The World now maintains a balance per namespace.
+When a system is called with value, the value stored in the World contract and credited to the system's namespace.
+
+Previously, the World contract did not store value, but passed it on to the system contracts.
+However, as systems are expected to be stateless (reading/writing state only via the calling World) and can be registered in multiple Worlds, this could have led to exploits.
+
+Any address with access to a namespace can use the balance of that namespace.
+This allows all systems registered in the same namespace to work with the same balance.
+
+There are two new World methods to transfer balance between namespaces (`transferBalanceToNamespace`) or to an address (`transferBalanceToAddress`).
+
+```solidity
+interface IBaseWorld {
+  function transferBalanceToNamespace(bytes16 fromNamespace, bytes16 toNamespace, uint256 amount) external;
+
+  function transferBalanceToAddress(bytes16 fromNamespace, address toAddress, uint256 amount) external;
+}
+```
diff --git a/packages/world/abi/BalanceTransferSystem.sol/BalanceTransferSystem.abi.json b/packages/world/abi/BalanceTransferSystem.sol/BalanceTransferSystem.abi.json
new file mode 100644
index 0000000000..4a4280c78f
--- /dev/null
+++ b/packages/world/abi/BalanceTransferSystem.sol/BalanceTransferSystem.abi.json
@@ -0,0 +1,237 @@
+[
+  {
+    "inputs": [
+      {
+        "internalType": "string",
+        "name": "resource",
+        "type": "string"
+      },
+      {
+        "internalType": "address",
+        "name": "caller",
+        "type": "address"
+      }
+    ],
+    "name": "AccessDenied",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "address",
+        "name": "delegator",
+        "type": "address"
+      },
+      {
+        "internalType": "address",
+        "name": "delegatee",
+        "type": "address"
+      }
+    ],
+    "name": "DelegationNotFound",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes4",
+        "name": "functionSelector",
+        "type": "bytes4"
+      }
+    ],
+    "name": "FunctionSelectorExists",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes4",
+        "name": "functionSelector",
+        "type": "bytes4"
+      }
+    ],
+    "name": "FunctionSelectorNotFound",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "balance",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "string",
+        "name": "resource",
+        "type": "string"
+      }
+    ],
+    "name": "InvalidSelector",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "string",
+        "name": "module",
+        "type": "string"
+      }
+    ],
+    "name": "ModuleAlreadyInstalled",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "length",
+        "type": "uint256"
+      }
+    ],
+    "name": "PackedCounter_InvalidLength",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "string",
+        "name": "resource",
+        "type": "string"
+      }
+    ],
+    "name": "ResourceExists",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "string",
+        "name": "resource",
+        "type": "string"
+      }
+    ],
+    "name": "ResourceNotFound",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "length",
+        "type": "uint256"
+      }
+    ],
+    "name": "SchemaLib_InvalidLength",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "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": "address",
+        "name": "system",
+        "type": "address"
+      }
+    ],
+    "name": "SystemExists",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes16",
+        "name": "fromNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "address",
+        "name": "toAddress",
+        "type": "address"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "transferBalanceToAddress",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes16",
+        "name": "fromNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "bytes16",
+        "name": "toNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "transferBalanceToNamespace",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  }
+]
\ No newline at end of file
diff --git a/packages/world/abi/BalanceTransferSystem.sol/BalanceTransferSystem.abi.json.d.ts b/packages/world/abi/BalanceTransferSystem.sol/BalanceTransferSystem.abi.json.d.ts
new file mode 100644
index 0000000000..4aa167e722
--- /dev/null
+++ b/packages/world/abi/BalanceTransferSystem.sol/BalanceTransferSystem.abi.json.d.ts
@@ -0,0 +1,238 @@
+declare const abi: [
+  {
+    inputs: [
+      {
+        internalType: "string";
+        name: "resource";
+        type: "string";
+      },
+      {
+        internalType: "address";
+        name: "caller";
+        type: "address";
+      }
+    ];
+    name: "AccessDenied";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "address";
+        name: "delegator";
+        type: "address";
+      },
+      {
+        internalType: "address";
+        name: "delegatee";
+        type: "address";
+      }
+    ];
+    name: "DelegationNotFound";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes4";
+        name: "functionSelector";
+        type: "bytes4";
+      }
+    ];
+    name: "FunctionSelectorExists";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes4";
+        name: "functionSelector";
+        type: "bytes4";
+      }
+    ];
+    name: "FunctionSelectorNotFound";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "balance";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "InsufficientBalance";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "string";
+        name: "resource";
+        type: "string";
+      }
+    ];
+    name: "InvalidSelector";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "string";
+        name: "module";
+        type: "string";
+      }
+    ];
+    name: "ModuleAlreadyInstalled";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "length";
+        type: "uint256";
+      }
+    ];
+    name: "PackedCounter_InvalidLength";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "string";
+        name: "resource";
+        type: "string";
+      }
+    ];
+    name: "ResourceExists";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "string";
+        name: "resource";
+        type: "string";
+      }
+    ];
+    name: "ResourceNotFound";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "length";
+        type: "uint256";
+      }
+    ];
+    name: "SchemaLib_InvalidLength";
+    type: "error";
+  },
+  {
+    inputs: [];
+    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: "address";
+        name: "system";
+        type: "address";
+      }
+    ];
+    name: "SystemExists";
+    type: "error";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes16";
+        name: "fromNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "address";
+        name: "toAddress";
+        type: "address";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "transferBalanceToAddress";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes16";
+        name: "fromNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "bytes16";
+        name: "toNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "transferBalanceToNamespace";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
+  }
+];
+export default abi;
diff --git a/packages/world/abi/Balances.sol/Balances.abi.json b/packages/world/abi/Balances.sol/Balances.abi.json
new file mode 100644
index 0000000000..0637a088a0
--- /dev/null
+++ b/packages/world/abi/Balances.sol/Balances.abi.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json
index d1a34a46a6..e3194498ac 100644
--- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json
+++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json
@@ -53,6 +53,22 @@
     "name": "FunctionSelectorNotFound",
     "type": "error"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "balance",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -528,6 +544,52 @@
     "stateMutability": "nonpayable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes16",
+        "name": "fromNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "address",
+        "name": "toAddress",
+        "type": "address"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "transferBalanceToAddress",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes16",
+        "name": "fromNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "bytes16",
+        "name": "toNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "transferBalanceToNamespace",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
   {
     "inputs": [
       {
diff --git a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts
index 2f28fea48f..1fc9762cd2 100644
--- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts
+++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts
@@ -53,6 +53,22 @@ declare const abi: [
     name: "FunctionSelectorNotFound";
     type: "error";
   },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "balance";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "InsufficientBalance";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -528,6 +544,52 @@ declare const abi: [
     stateMutability: "nonpayable";
     type: "function";
   },
+  {
+    inputs: [
+      {
+        internalType: "bytes16";
+        name: "fromNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "address";
+        name: "toAddress";
+        type: "address";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "transferBalanceToAddress";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes16";
+        name: "fromNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "bytes16";
+        name: "toNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "transferBalanceToNamespace";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
+  },
   {
     inputs: [
       {
diff --git a/packages/world/abi/IBalanceTransferSystem.sol/IBalanceTransferSystem.abi.json b/packages/world/abi/IBalanceTransferSystem.sol/IBalanceTransferSystem.abi.json
new file mode 100644
index 0000000000..c344a5cad3
--- /dev/null
+++ b/packages/world/abi/IBalanceTransferSystem.sol/IBalanceTransferSystem.abi.json
@@ -0,0 +1,48 @@
+[
+  {
+    "inputs": [
+      {
+        "internalType": "bytes16",
+        "name": "fromNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "address",
+        "name": "toAddress",
+        "type": "address"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "transferBalanceToAddress",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes16",
+        "name": "fromNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "bytes16",
+        "name": "toNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "transferBalanceToNamespace",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  }
+]
\ No newline at end of file
diff --git a/packages/world/abi/IBalanceTransferSystem.sol/IBalanceTransferSystem.abi.json.d.ts b/packages/world/abi/IBalanceTransferSystem.sol/IBalanceTransferSystem.abi.json.d.ts
new file mode 100644
index 0000000000..ab006fbb25
--- /dev/null
+++ b/packages/world/abi/IBalanceTransferSystem.sol/IBalanceTransferSystem.abi.json.d.ts
@@ -0,0 +1,49 @@
+declare const abi: [
+  {
+    inputs: [
+      {
+        internalType: "bytes16";
+        name: "fromNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "address";
+        name: "toAddress";
+        type: "address";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "transferBalanceToAddress";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes16";
+        name: "fromNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "bytes16";
+        name: "toNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "transferBalanceToNamespace";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
+  }
+];
+export default abi;
diff --git a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json
index 48cc763e25..50884b02cc 100644
--- a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json
+++ b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json
@@ -53,6 +53,22 @@
     "name": "FunctionSelectorNotFound",
     "type": "error"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "balance",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
   {
     "inputs": [
       {
@@ -998,6 +1014,52 @@
     "stateMutability": "nonpayable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes16",
+        "name": "fromNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "address",
+        "name": "toAddress",
+        "type": "address"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "transferBalanceToAddress",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes16",
+        "name": "fromNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "bytes16",
+        "name": "toNamespace",
+        "type": "bytes16"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "transferBalanceToNamespace",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
   {
     "inputs": [
       {
diff --git a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts
index ee8ae36caa..5ee4ca34f2 100644
--- a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts
+++ b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts
@@ -53,6 +53,22 @@ declare const abi: [
     name: "FunctionSelectorNotFound";
     type: "error";
   },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "balance";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "InsufficientBalance";
+    type: "error";
+  },
   {
     inputs: [
       {
@@ -998,6 +1014,52 @@ declare const abi: [
     stateMutability: "nonpayable";
     type: "function";
   },
+  {
+    inputs: [
+      {
+        internalType: "bytes16";
+        name: "fromNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "address";
+        name: "toAddress";
+        type: "address";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "transferBalanceToAddress";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes16";
+        name: "fromNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "bytes16";
+        name: "toNamespace";
+        type: "bytes16";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "transferBalanceToNamespace";
+    outputs: [];
+    stateMutability: "nonpayable";
+    type: "function";
+  },
   {
     inputs: [
       {
diff --git a/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json b/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json
index e1b0321916..ea3fca584b 100644
--- a/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json
+++ b/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json
@@ -53,6 +53,22 @@
     "name": "FunctionSelectorNotFound",
     "type": "error"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "balance",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
   {
     "inputs": [
       {
diff --git a/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json.d.ts b/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json.d.ts
index c75b1df47d..14acc066fd 100644
--- a/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json.d.ts
+++ b/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json.d.ts
@@ -53,6 +53,22 @@ declare const abi: [
     name: "FunctionSelectorNotFound";
     type: "error";
   },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "balance";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "InsufficientBalance";
+    type: "error";
+  },
   {
     inputs: [
       {
diff --git a/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json b/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json
index ca66fdee6c..393e339799 100644
--- a/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json
+++ b/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json
@@ -53,6 +53,22 @@
     "name": "FunctionSelectorNotFound",
     "type": "error"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "balance",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
   {
     "inputs": [
       {
diff --git a/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json.d.ts b/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json.d.ts
index e1453a39e3..c5dbe952a9 100644
--- a/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json.d.ts
+++ b/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json.d.ts
@@ -53,6 +53,22 @@ declare const abi: [
     name: "FunctionSelectorNotFound";
     type: "error";
   },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "balance";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "InsufficientBalance";
+    type: "error";
+  },
   {
     inputs: [
       {
diff --git a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json
index c312a17e90..5c2c6fb55e 100644
--- a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json
+++ b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json
@@ -53,6 +53,22 @@
     "name": "FunctionSelectorNotFound",
     "type": "error"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "balance",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
   {
     "inputs": [
       {
diff --git a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts
index eec6fa9c6b..9c27c02ca1 100644
--- a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts
+++ b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts
@@ -53,6 +53,22 @@ declare const abi: [
     name: "FunctionSelectorNotFound";
     type: "error";
   },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "balance";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "InsufficientBalance";
+    type: "error";
+  },
   {
     inputs: [
       {
diff --git a/packages/world/abi/World.sol/World.abi.json b/packages/world/abi/World.sol/World.abi.json
index 42c837564a..f4e0e2d4c7 100644
--- a/packages/world/abi/World.sol/World.abi.json
+++ b/packages/world/abi/World.sol/World.abi.json
@@ -58,6 +58,22 @@
     "name": "FunctionSelectorNotFound",
     "type": "error"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "balance",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
   {
     "inputs": [
       {
diff --git a/packages/world/abi/World.sol/World.abi.json.d.ts b/packages/world/abi/World.sol/World.abi.json.d.ts
index fe43c93074..865d2dd465 100644
--- a/packages/world/abi/World.sol/World.abi.json.d.ts
+++ b/packages/world/abi/World.sol/World.abi.json.d.ts
@@ -58,6 +58,22 @@ declare const abi: [
     name: "FunctionSelectorNotFound";
     type: "error";
   },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "balance";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "InsufficientBalance";
+    type: "error";
+  },
   {
     inputs: [
       {
diff --git a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json
index 2f527ba15b..22d93fb963 100644
--- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json
+++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json
@@ -53,6 +53,22 @@
     "name": "FunctionSelectorNotFound",
     "type": "error"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "balance",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
   {
     "inputs": [
       {
diff --git a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts
index 37c3dda709..89e7d11d89 100644
--- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts
+++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts
@@ -53,6 +53,22 @@ declare const abi: [
     name: "FunctionSelectorNotFound";
     type: "error";
   },
+  {
+    inputs: [
+      {
+        internalType: "uint256";
+        name: "balance";
+        type: "uint256";
+      },
+      {
+        internalType: "uint256";
+        name: "amount";
+        type: "uint256";
+      }
+    ];
+    name: "InsufficientBalance";
+    type: "error";
+  },
   {
     inputs: [
       {
diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json
index ec99ed9234..0029f237db 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": 1412624
+    "gasUsed": 1413349
   },
   {
     "file": "test/KeysInTableModule.t.sol",
     "test": "testInstallGas",
     "name": "install keys in table module",
-    "gasUsed": 1412624
+    "gasUsed": 1413349
   },
   {
     "file": "test/KeysInTableModule.t.sol",
@@ -57,13 +57,13 @@
     "file": "test/KeysInTableModule.t.sol",
     "test": "testInstallSingleton",
     "name": "install keys in table module",
-    "gasUsed": 1412624
+    "gasUsed": 1413349
   },
   {
     "file": "test/KeysInTableModule.t.sol",
     "test": "testSetAndDeleteRecordHookCompositeGas",
     "name": "install keys in table module",
-    "gasUsed": 1412624
+    "gasUsed": 1413349
   },
   {
     "file": "test/KeysInTableModule.t.sol",
@@ -81,7 +81,7 @@
     "file": "test/KeysInTableModule.t.sol",
     "test": "testSetAndDeleteRecordHookGas",
     "name": "install keys in table module",
-    "gasUsed": 1412624
+    "gasUsed": 1413349
   },
   {
     "file": "test/KeysInTableModule.t.sol",
@@ -99,7 +99,7 @@
     "file": "test/KeysWithValueModule.t.sol",
     "test": "testGetKeysWithValueGas",
     "name": "install keys with value module",
-    "gasUsed": 651087
+    "gasUsed": 651540
   },
   {
     "file": "test/KeysWithValueModule.t.sol",
@@ -117,7 +117,7 @@
     "file": "test/KeysWithValueModule.t.sol",
     "test": "testInstall",
     "name": "install keys with value module",
-    "gasUsed": 651087
+    "gasUsed": 651540
   },
   {
     "file": "test/KeysWithValueModule.t.sol",
@@ -129,7 +129,7 @@
     "file": "test/KeysWithValueModule.t.sol",
     "test": "testSetAndDeleteRecordHook",
     "name": "install keys with value module",
-    "gasUsed": 651087
+    "gasUsed": 651540
   },
   {
     "file": "test/KeysWithValueModule.t.sol",
@@ -147,7 +147,7 @@
     "file": "test/KeysWithValueModule.t.sol",
     "test": "testSetField",
     "name": "install keys with value module",
-    "gasUsed": 651087
+    "gasUsed": 651540
   },
   {
     "file": "test/KeysWithValueModule.t.sol",
@@ -165,31 +165,31 @@
     "file": "test/query.t.sol",
     "test": "testCombinedHasHasValueNotQuery",
     "name": "CombinedHasHasValueNotQuery",
-    "gasUsed": 165765
+    "gasUsed": 165699
   },
   {
     "file": "test/query.t.sol",
     "test": "testCombinedHasHasValueQuery",
     "name": "CombinedHasHasValueQuery",
-    "gasUsed": 76018
+    "gasUsed": 75996
   },
   {
     "file": "test/query.t.sol",
     "test": "testCombinedHasNotQuery",
     "name": "CombinedHasNotQuery",
-    "gasUsed": 229965
+    "gasUsed": 229855
   },
   {
     "file": "test/query.t.sol",
     "test": "testCombinedHasQuery",
     "name": "CombinedHasQuery",
-    "gasUsed": 151676
+    "gasUsed": 151588
   },
   {
     "file": "test/query.t.sol",
     "test": "testCombinedHasValueNotQuery",
     "name": "CombinedHasValueNotQuery",
-    "gasUsed": 143536
+    "gasUsed": 143470
   },
   {
     "file": "test/query.t.sol",
@@ -201,19 +201,19 @@
     "file": "test/query.t.sol",
     "test": "testHasQuery",
     "name": "HasQuery",
-    "gasUsed": 34905
+    "gasUsed": 34883
   },
   {
     "file": "test/query.t.sol",
     "test": "testHasQuery1000Keys",
     "name": "HasQuery with 1000 keys",
-    "gasUsed": 9272667
+    "gasUsed": 9272645
   },
   {
     "file": "test/query.t.sol",
     "test": "testHasQuery100Keys",
     "name": "HasQuery with 100 keys",
-    "gasUsed": 861400
+    "gasUsed": 861378
   },
   {
     "file": "test/query.t.sol",
@@ -225,73 +225,73 @@
     "file": "test/query.t.sol",
     "test": "testNotValueQuery",
     "name": "NotValueQuery",
-    "gasUsed": 69612
+    "gasUsed": 69590
   },
   {
     "file": "test/StandardDelegationsModule.t.sol",
     "test": "testCallFromCallboundDelegation",
     "name": "register a callbound delegation",
-    "gasUsed": 122373
+    "gasUsed": 122630
   },
   {
     "file": "test/StandardDelegationsModule.t.sol",
     "test": "testCallFromCallboundDelegation",
     "name": "call a system via a callbound delegation",
-    "gasUsed": 44114
+    "gasUsed": 44360
   },
   {
     "file": "test/StandardDelegationsModule.t.sol",
     "test": "testCallFromTimeboundDelegation",
     "name": "register a timebound delegation",
-    "gasUsed": 116622
+    "gasUsed": 116879
   },
   {
     "file": "test/StandardDelegationsModule.t.sol",
     "test": "testCallFromTimeboundDelegation",
     "name": "call a system via a timebound delegation",
-    "gasUsed": 34819
+    "gasUsed": 35065
   },
   {
     "file": "test/UniqueEntityModule.t.sol",
     "test": "testInstall",
     "name": "install unique entity module",
-    "gasUsed": 726567
+    "gasUsed": 727309
   },
   {
     "file": "test/UniqueEntityModule.t.sol",
     "test": "testInstall",
     "name": "get a unique entity nonce (non-root module)",
-    "gasUsed": 65194
+    "gasUsed": 65315
   },
   {
     "file": "test/UniqueEntityModule.t.sol",
     "test": "testInstallRoot",
     "name": "installRoot unique entity module",
-    "gasUsed": 705483
+    "gasUsed": 706087
   },
   {
     "file": "test/UniqueEntityModule.t.sol",
     "test": "testInstallRoot",
     "name": "get a unique entity nonce (root module)",
-    "gasUsed": 65194
+    "gasUsed": 65315
   },
   {
     "file": "test/World.t.sol",
     "test": "testCall",
     "name": "call a system via the World",
-    "gasUsed": 17519
+    "gasUsed": 17637
   },
   {
     "file": "test/World.t.sol",
     "test": "testCallFromUnlimitedDelegation",
     "name": "register an unlimited delegation",
-    "gasUsed": 55485
+    "gasUsed": 55612
   },
   {
     "file": "test/World.t.sol",
     "test": "testCallFromUnlimitedDelegation",
     "name": "call a system via an unlimited delegation",
-    "gasUsed": 17893
+    "gasUsed": 18011
   },
   {
     "file": "test/World.t.sol",
@@ -309,37 +309,37 @@
     "file": "test/World.t.sol",
     "test": "testRegisterFallbackSystem",
     "name": "Register a fallback system",
-    "gasUsed": 70471
+    "gasUsed": 70620
   },
   {
     "file": "test/World.t.sol",
     "test": "testRegisterFallbackSystem",
     "name": "Register a root fallback system",
-    "gasUsed": 63733
+    "gasUsed": 63860
   },
   {
     "file": "test/World.t.sol",
     "test": "testRegisterFunctionSelector",
     "name": "Register a function selector",
-    "gasUsed": 91065
+    "gasUsed": 91214
   },
   {
     "file": "test/World.t.sol",
     "test": "testRegisterNamespace",
     "name": "Register a new namespace",
-    "gasUsed": 140158
+    "gasUsed": 140240
   },
   {
     "file": "test/World.t.sol",
     "test": "testRegisterRootFunctionSelector",
     "name": "Register a root function selector",
-    "gasUsed": 79644
+    "gasUsed": 79771
   },
   {
     "file": "test/World.t.sol",
     "test": "testRegisterTable",
     "name": "Register a new table in the namespace",
-    "gasUsed": 650278
+    "gasUsed": 650471
   },
   {
     "file": "test/World.t.sol",
diff --git a/packages/world/mud.config.ts b/packages/world/mud.config.ts
index 56dc6e701e..b23b1e0919 100644
--- a/packages/world/mud.config.ts
+++ b/packages/world/mud.config.ts
@@ -56,6 +56,15 @@ export default mudConfig({
      *    MODULE TABLES
      *
      ************************************************************************/
+    Balances: {
+      directory: "modules/core/tables",
+      keySchema: {
+        namespace: "bytes16",
+      },
+      schema: {
+        balance: "uint256",
+      },
+    },
     Systems: {
       directory: "modules/core/tables",
       keySchema: {
diff --git a/packages/world/src/SystemCall.sol b/packages/world/src/SystemCall.sol
index 4750b20f59..d5bdf7f53f 100644
--- a/packages/world/src/SystemCall.sol
+++ b/packages/world/src/SystemCall.sol
@@ -18,6 +18,7 @@ import { ISystemHook } from "./interfaces/ISystemHook.sol";
 import { FunctionSelectors } from "./modules/core/tables/FunctionSelectors.sol";
 import { Systems } from "./modules/core/tables/Systems.sol";
 import { SystemHooks } from "./modules/core/tables/SystemHooks.sol";
+import { Balances } from "./modules/core/tables/Balances.sol";
 
 library SystemCall {
   using ResourceSelector for bytes32;
@@ -28,9 +29,9 @@ library SystemCall {
    */
   function call(
     address caller,
+    uint256 value,
     bytes32 resourceSelector,
-    bytes memory funcSelectorAndArgs,
-    uint256 value
+    bytes memory funcSelectorAndArgs
   ) internal returns (bool success, bytes memory data) {
     // Load the system data
     (address systemAddress, bool publicAccess) = Systems.get(resourceSelector);
@@ -41,18 +42,26 @@ library SystemCall {
     // Allow access if the system is public or the caller has access to the namespace or name
     if (!publicAccess) AccessControl.requireAccess(resourceSelector, caller);
 
+    // If the msg.value is non-zero, update the namespace's balance
+    if (value > 0) {
+      bytes16 namespace = resourceSelector.getNamespace();
+      uint256 currentBalance = Balances.get(namespace);
+      Balances.set(namespace, currentBalance + value);
+    }
+
     // Call the system and forward any return data
     (success, data) = resourceSelector.getNamespace() == ROOT_NAMESPACE // Use delegatecall for root systems (= registered in the root namespace)
       ? WorldContextProvider.delegatecallWithContext({
         msgSender: caller,
+        msgValue: value,
         target: systemAddress,
         funcSelectorAndArgs: funcSelectorAndArgs
       })
       : WorldContextProvider.callWithContext({
         msgSender: caller,
+        msgValue: value,
         target: systemAddress,
-        funcSelectorAndArgs: funcSelectorAndArgs,
-        value: value
+        funcSelectorAndArgs: funcSelectorAndArgs
       });
   }
 
@@ -78,7 +87,12 @@ library SystemCall {
     }
 
     // Call the system and forward any return data
-    (success, data) = call(caller, resourceSelector, funcSelectorAndArgs, value);
+    (success, data) = call({
+      caller: caller,
+      value: value,
+      resourceSelector: resourceSelector,
+      funcSelectorAndArgs: funcSelectorAndArgs
+    });
 
     // Call onAfterCallSystem hooks (after calling the system)
     for (uint256 i; i < hooks.length; i++) {
@@ -99,7 +113,12 @@ library SystemCall {
     bytes memory funcSelectorAndArgs,
     uint256 value
   ) internal returns (bytes memory data) {
-    (bool success, bytes memory returnData) = callWithHooks(caller, resourceSelector, funcSelectorAndArgs, value);
+    (bool success, bytes memory returnData) = callWithHooks({
+      caller: caller,
+      value: value,
+      resourceSelector: resourceSelector,
+      funcSelectorAndArgs: funcSelectorAndArgs
+    });
     if (!success) revertWithBytes(returnData);
     return returnData;
   }
diff --git a/packages/world/src/Tables.sol b/packages/world/src/Tables.sol
index 40e2a3aa48..27d8009070 100644
--- a/packages/world/src/Tables.sol
+++ b/packages/world/src/Tables.sol
@@ -7,6 +7,7 @@ import { NamespaceOwner, NamespaceOwnerTableId } from "./tables/NamespaceOwner.s
 import { ResourceAccess, ResourceAccessTableId } from "./tables/ResourceAccess.sol";
 import { InstalledModules, InstalledModulesData, InstalledModulesTableId } from "./tables/InstalledModules.sol";
 import { Delegations, DelegationsTableId } from "./tables/Delegations.sol";
+import { Balances, BalancesTableId } from "./modules/core/tables/Balances.sol";
 import { Systems, SystemsTableId } from "./modules/core/tables/Systems.sol";
 import { SystemRegistry, SystemRegistryTableId } from "./modules/core/tables/SystemRegistry.sol";
 import { SystemHooks, SystemHooksTableId } from "./modules/core/tables/SystemHooks.sol";
diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol
index 2e4f67ee2b..8f5256ef75 100644
--- a/packages/world/src/World.sol
+++ b/packages/world/src/World.sol
@@ -28,6 +28,7 @@ import { IDelegationControl } from "./interfaces/IDelegationControl.sol";
 import { Systems } from "./modules/core/tables/Systems.sol";
 import { SystemHooks } from "./modules/core/tables/SystemHooks.sol";
 import { FunctionSelectors } from "./modules/core/tables/FunctionSelectors.sol";
+import { Balances } from "./modules/core/tables/Balances.sol";
 
 contract World is StoreRead, IStoreData, IWorldKernel {
   using ResourceSelector for bytes32;
@@ -39,8 +40,6 @@ contract World is StoreRead, IStoreData, IWorldKernel {
     NamespaceOwner.register();
     NamespaceOwner.set(ROOT_NAMESPACE, msg.sender);
 
-    // Other internal tables are registered by the CoreModule to reduce World's bytecode size.
-
     emit HelloWorld();
   }
 
@@ -54,6 +53,7 @@ contract World is StoreRead, IStoreData, IWorldKernel {
 
     WorldContextProvider.delegatecallWithContextOrRevert({
       msgSender: msg.sender,
+      msgValue: 0,
       target: address(module),
       funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, args)
     });
@@ -221,9 +221,12 @@ contract World is StoreRead, IStoreData, IWorldKernel {
    ************************************************************************/
 
   /**
-   * Allow the World to receive ETH
+   * ETH sent to the World without calldata is added to the root namespace's balance
    */
-  receive() external payable {}
+  receive() external payable {
+    uint256 rootBalance = Balances.get(ROOT_NAMESPACE);
+    Balances.set(ROOT_NAMESPACE, rootBalance + msg.value);
+  }
 
   /**
    * Fallback function to call registered function selectors
diff --git a/packages/world/src/WorldContext.sol b/packages/world/src/WorldContext.sol
index ff5bd964c5..06c5a22d07 100644
--- a/packages/world/src/WorldContext.sol
+++ b/packages/world/src/WorldContext.sol
@@ -4,18 +4,31 @@ pragma solidity >=0.8.0;
 import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
 import { revertWithBytes } from "./revertWithBytes.sol";
 
+// The context size is 20 bytes for msg.sender, and 32 bytes for msg.value
+uint256 constant CONTEXT_BYTES = 20 + 32;
+
 // Similar to https://eips.ethereum.org/EIPS/eip-2771, but any contract can be the trusted forwarder.
 // This should only be used for contracts without own storage, like Systems.
 abstract contract WorldContextConsumer {
   // Extract the trusted msg.sender value appended to the calldata
   function _msgSender() internal view returns (address sender) {
     assembly {
+      // Load 32 bytes from calldata at position calldatasize() - context size,
+      // then shift left 96 bits (to right-align the address)
       // 96 = 256 - 20 * 8
-      sender := shr(96, calldataload(sub(calldatasize(), 20)))
+      sender := shr(96, calldataload(sub(calldatasize(), CONTEXT_BYTES)))
     }
     if (sender == address(0)) sender = msg.sender;
   }
 
+  // Extract the trusted msg.value value appended to the calldata
+  function _msgValue() internal pure returns (uint256 value) {
+    assembly {
+      // Load 32 bytes from calldata at position calldatasize() - 32 bytes,
+      value := calldataload(sub(calldatasize(), 32))
+    }
+  }
+
   function _world() internal view returns (address) {
     return StoreSwitch.getStoreAddress();
   }
@@ -25,44 +38,64 @@ abstract contract WorldContextConsumer {
  * Simple utility function to call a contract and append the msg.sender to the calldata (to be consumed by WorldContextConsumer)
  */
 library WorldContextProvider {
-  function appendContext(bytes memory funcSelectorAndArgs, address msgSender) internal pure returns (bytes memory) {
-    return abi.encodePacked(funcSelectorAndArgs, msgSender);
+  function appendContext(
+    bytes memory funcSelectorAndArgs,
+    address msgSender,
+    uint256 msgValue
+  ) internal pure returns (bytes memory) {
+    return abi.encodePacked(funcSelectorAndArgs, msgSender, msgValue);
   }
 
   function callWithContext(
-    address target,
-    bytes memory funcSelectorAndArgs,
     address msgSender,
-    uint256 value
+    uint256 msgValue,
+    address target,
+    bytes memory funcSelectorAndArgs
   ) internal returns (bool success, bytes memory data) {
-    (success, data) = target.call{ value: value }(appendContext(funcSelectorAndArgs, msgSender));
+    (success, data) = target.call{ value: 0 }(
+      appendContext({ funcSelectorAndArgs: funcSelectorAndArgs, msgSender: msgSender, msgValue: msgValue })
+    );
   }
 
   function delegatecallWithContext(
+    address msgSender,
+    uint256 msgValue,
     address target,
-    bytes memory funcSelectorAndArgs,
-    address msgSender
+    bytes memory funcSelectorAndArgs
   ) internal returns (bool success, bytes memory data) {
-    (success, data) = target.delegatecall(appendContext(funcSelectorAndArgs, msgSender));
+    (success, data) = target.delegatecall(
+      appendContext({ funcSelectorAndArgs: funcSelectorAndArgs, msgSender: msgSender, msgValue: msgValue })
+    );
   }
 
   function callWithContextOrRevert(
-    address target,
-    bytes memory funcSelectorAndArgs,
     address msgSender,
-    uint256 value
+    uint256 msgValue,
+    address target,
+    bytes memory funcSelectorAndArgs
   ) internal returns (bytes memory data) {
-    (bool success, bytes memory _data) = callWithContext(target, funcSelectorAndArgs, msgSender, value);
+    (bool success, bytes memory _data) = callWithContext({
+      msgSender: msgSender,
+      msgValue: msgValue,
+      target: target,
+      funcSelectorAndArgs: funcSelectorAndArgs
+    });
     if (!success) revertWithBytes(_data);
     return _data;
   }
 
   function delegatecallWithContextOrRevert(
+    address msgSender,
+    uint256 msgValue,
     address target,
-    bytes memory funcSelectorAndArgs,
-    address msgSender
+    bytes memory funcSelectorAndArgs
   ) internal returns (bytes memory data) {
-    (bool success, bytes memory _data) = delegatecallWithContext(target, funcSelectorAndArgs, msgSender);
+    (bool success, bytes memory _data) = delegatecallWithContext({
+      msgSender: msgSender,
+      msgValue: msgValue,
+      target: target,
+      funcSelectorAndArgs: funcSelectorAndArgs
+    });
     if (!success) revertWithBytes(_data);
     return _data;
   }
diff --git a/packages/world/src/interfaces/IBalanceTransferSystem.sol b/packages/world/src/interfaces/IBalanceTransferSystem.sol
new file mode 100644
index 0000000000..eddb986957
--- /dev/null
+++ b/packages/world/src/interfaces/IBalanceTransferSystem.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+/* Autogenerated file. Do not edit manually. */
+
+interface IBalanceTransferSystem {
+  function transferBalanceToNamespace(bytes16 fromNamespace, bytes16 toNamespace, uint256 amount) external;
+
+  function transferBalanceToAddress(bytes16 fromNamespace, address toAddress, uint256 amount) external;
+}
diff --git a/packages/world/src/interfaces/IBaseWorld.sol b/packages/world/src/interfaces/IBaseWorld.sol
index 88806d4364..dcc9607777 100644
--- a/packages/world/src/interfaces/IBaseWorld.sol
+++ b/packages/world/src/interfaces/IBaseWorld.sol
@@ -8,6 +8,7 @@ import { IWorldKernel } from "../interfaces/IWorldKernel.sol";
 
 import { ICoreSystem } from "./ICoreSystem.sol";
 import { IAccessManagementSystem } from "./IAccessManagementSystem.sol";
+import { IBalanceTransferSystem } from "./IBalanceTransferSystem.sol";
 import { IModuleInstallationSystem } from "./IModuleInstallationSystem.sol";
 import { IWorldRegistrationSystem } from "./IWorldRegistrationSystem.sol";
 
@@ -20,6 +21,7 @@ interface IBaseWorld is
   IWorldKernel,
   ICoreSystem,
   IAccessManagementSystem,
+  IBalanceTransferSystem,
   IModuleInstallationSystem,
   IWorldRegistrationSystem
 {
diff --git a/packages/world/src/interfaces/IWorldErrors.sol b/packages/world/src/interfaces/IWorldErrors.sol
index 984ee282bb..918f1c548c 100644
--- a/packages/world/src/interfaces/IWorldErrors.sol
+++ b/packages/world/src/interfaces/IWorldErrors.sol
@@ -11,4 +11,5 @@ interface IWorldErrors {
   error FunctionSelectorNotFound(bytes4 functionSelector);
   error ModuleAlreadyInstalled(string module);
   error DelegationNotFound(address delegator, address delegatee);
+  error InsufficientBalance(uint256 balance, uint256 amount);
 }
diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol
index 74bb71585e..72842d9c84 100644
--- a/packages/world/src/modules/core/CoreModule.sol
+++ b/packages/world/src/modules/core/CoreModule.sol
@@ -24,12 +24,14 @@ import { FunctionSelectors } from "./tables/FunctionSelectors.sol";
 import { ResourceType } from "./tables/ResourceType.sol";
 import { SystemHooks } from "./tables/SystemHooks.sol";
 import { SystemRegistry } from "./tables/SystemRegistry.sol";
+import { Balances } from "./tables/Balances.sol";
 
-import { WorldRegistrationSystem } from "./implementations/WorldRegistrationSystem.sol";
-import { StoreRegistrationSystem } from "./implementations/StoreRegistrationSystem.sol";
-import { ModuleInstallationSystem } from "./implementations/ModuleInstallationSystem.sol";
 import { AccessManagementSystem } from "./implementations/AccessManagementSystem.sol";
+import { BalanceTransferSystem } from "./implementations/BalanceTransferSystem.sol";
 import { EphemeralRecordSystem } from "./implementations/EphemeralRecordSystem.sol";
+import { ModuleInstallationSystem } from "./implementations/ModuleInstallationSystem.sol";
+import { StoreRegistrationSystem } from "./implementations/StoreRegistrationSystem.sol";
+import { WorldRegistrationSystem } from "./implementations/WorldRegistrationSystem.sol";
 
 /**
  * The CoreModule registers internal World tables, the CoreSystem, and its function selectors.
@@ -57,6 +59,7 @@ contract CoreModule is IModule, WorldContextConsumer {
    * Register core tables in the World
    */
   function _registerCoreTables() internal {
+    Balances.register();
     InstalledModules.register();
     ResourceAccess.register();
     Systems.register();
@@ -76,6 +79,7 @@ contract CoreModule is IModule, WorldContextConsumer {
     // Use the CoreSystem's `registerSystem` implementation to register itself on the World.
     WorldContextProvider.delegatecallWithContextOrRevert({
       msgSender: _msgSender(),
+      msgValue: 0,
       target: coreSystem,
       funcSelectorAndArgs: abi.encodeWithSelector(
         WorldRegistrationSystem.registerSystem.selector,
@@ -90,7 +94,22 @@ contract CoreModule is IModule, WorldContextConsumer {
    * Register function selectors for all CoreSystem functions in the World
    */
   function _registerFunctionSelectors() internal {
-    bytes4[15] memory functionSelectors = [
+    bytes4[17] memory functionSelectors = [
+      // --- AccessManagementSystem ---
+      AccessManagementSystem.grantAccess.selector,
+      AccessManagementSystem.revokeAccess.selector,
+      AccessManagementSystem.transferOwnership.selector,
+      // --- BalanceTransferSystem ---
+      BalanceTransferSystem.transferBalanceToNamespace.selector,
+      BalanceTransferSystem.transferBalanceToAddress.selector,
+      // --- EphemeralRecordSystem ---
+      IStoreEphemeral.emitEphemeralRecord.selector,
+      // --- ModuleInstallationSystem ---
+      ModuleInstallationSystem.installModule.selector,
+      // --- StoreRegistrationSystem ---
+      StoreRegistrationSystem.registerTable.selector,
+      StoreRegistrationSystem.registerStoreHook.selector,
+      StoreRegistrationSystem.unregisterStoreHook.selector,
       // --- WorldRegistrationSystem ---
       WorldRegistrationSystem.registerNamespace.selector,
       WorldRegistrationSystem.registerSystemHook.selector,
@@ -98,19 +117,7 @@ contract CoreModule is IModule, WorldContextConsumer {
       WorldRegistrationSystem.registerSystem.selector,
       WorldRegistrationSystem.registerFunctionSelector.selector,
       WorldRegistrationSystem.registerRootFunctionSelector.selector,
-      WorldRegistrationSystem.registerDelegation.selector,
-      // --- StoreRegistrationSystem ---
-      StoreRegistrationSystem.registerTable.selector,
-      StoreRegistrationSystem.registerStoreHook.selector,
-      StoreRegistrationSystem.unregisterStoreHook.selector,
-      // --- ModuleInstallationSystem ---
-      ModuleInstallationSystem.installModule.selector,
-      // --- AccessManagementSystem ---
-      AccessManagementSystem.grantAccess.selector,
-      AccessManagementSystem.revokeAccess.selector,
-      AccessManagementSystem.transferOwnership.selector,
-      // --- EphemeralRecordSystem ---
-      IStoreEphemeral.emitEphemeralRecord.selector
+      WorldRegistrationSystem.registerDelegation.selector
     ];
 
     for (uint256 i = 0; i < functionSelectors.length; i++) {
@@ -118,6 +125,7 @@ contract CoreModule is IModule, WorldContextConsumer {
       // root function selectors in the World.
       WorldContextProvider.delegatecallWithContextOrRevert({
         msgSender: _msgSender(),
+        msgValue: 0,
         target: coreSystem,
         funcSelectorAndArgs: abi.encodeWithSelector(
           WorldRegistrationSystem.registerRootFunctionSelector.selector,
diff --git a/packages/world/src/modules/core/CoreSystem.sol b/packages/world/src/modules/core/CoreSystem.sol
index 368558f929..1a3b5c0c1c 100644
--- a/packages/world/src/modules/core/CoreSystem.sol
+++ b/packages/world/src/modules/core/CoreSystem.sol
@@ -3,11 +3,12 @@ pragma solidity >=0.8.0;
 
 import { IWorldErrors } from "../../interfaces/IWorldErrors.sol";
 
-import { StoreRegistrationSystem } from "./implementations/StoreRegistrationSystem.sol";
-import { WorldRegistrationSystem } from "./implementations/WorldRegistrationSystem.sol";
-import { ModuleInstallationSystem } from "./implementations/ModuleInstallationSystem.sol";
 import { AccessManagementSystem } from "./implementations/AccessManagementSystem.sol";
+import { BalanceTransferSystem } from "./implementations/BalanceTransferSystem.sol";
 import { EphemeralRecordSystem } from "./implementations/EphemeralRecordSystem.sol";
+import { ModuleInstallationSystem } from "./implementations/ModuleInstallationSystem.sol";
+import { StoreRegistrationSystem } from "./implementations/StoreRegistrationSystem.sol";
+import { WorldRegistrationSystem } from "./implementations/WorldRegistrationSystem.sol";
 
 /**
  * The CoreSystem includes all World functionality that is externalized
@@ -15,11 +16,12 @@ import { EphemeralRecordSystem } from "./implementations/EphemeralRecordSystem.s
  */
 contract CoreSystem is
   IWorldErrors,
-  StoreRegistrationSystem,
-  WorldRegistrationSystem,
-  ModuleInstallationSystem,
   AccessManagementSystem,
-  EphemeralRecordSystem
+  BalanceTransferSystem,
+  EphemeralRecordSystem,
+  ModuleInstallationSystem,
+  StoreRegistrationSystem,
+  WorldRegistrationSystem
 {
 
 }
diff --git a/packages/world/src/modules/core/implementations/BalanceTransferSystem.sol b/packages/world/src/modules/core/implementations/BalanceTransferSystem.sol
new file mode 100644
index 0000000000..d57cc82a6c
--- /dev/null
+++ b/packages/world/src/modules/core/implementations/BalanceTransferSystem.sol
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+import { System } from "../../../System.sol";
+import { revertWithBytes } from "../../../revertWithBytes.sol";
+import { ResourceSelector } from "../../../ResourceSelector.sol";
+import { AccessControl } from "../../../AccessControl.sol";
+import { IWorldErrors } from "../../../interfaces/IWorldErrors.sol";
+
+import { Balances } from "../tables/Balances.sol";
+
+contract BalanceTransferSystem is System, IWorldErrors {
+  using ResourceSelector for bytes32;
+
+  /**
+   * Transfer balance to another namespace in the World
+   */
+  function transferBalanceToNamespace(bytes16 fromNamespace, bytes16 toNamespace, uint256 amount) public virtual {
+    // Require caller to have access to the namespace
+    AccessControl.requireAccess(fromNamespace, _msgSender());
+
+    // Get current namespace balance
+    uint256 balance = Balances.get(fromNamespace);
+
+    // Require the balance balance to be greater or equal to the amount to transfer
+    if (amount > balance) revert InsufficientBalance(balance, amount);
+
+    // Update the balances
+    Balances.set(fromNamespace, balance - amount);
+    Balances.set(toNamespace, Balances.get(toNamespace) + amount);
+  }
+
+  /**
+   * Transfer balance out of the World
+   */
+  function transferBalanceToAddress(bytes16 fromNamespace, address toAddress, uint256 amount) public virtual {
+    // Require caller to have access to the namespace
+    AccessControl.requireAccess(fromNamespace, _msgSender());
+
+    // Get current namespace balance
+    uint256 balance = Balances.get(fromNamespace);
+
+    // Require the balance balance to be greater or equal to the amount to transfer
+    if (amount > balance) revert InsufficientBalance(balance, amount);
+
+    // Update the balances
+    Balances.set(fromNamespace, balance - amount);
+
+    // Transfer the balance to the given address, revert on failure
+    (bool success, bytes memory data) = payable(toAddress).call{ value: amount }("");
+    if (!success) revertWithBytes(data);
+  }
+}
diff --git a/packages/world/src/modules/core/implementations/ModuleInstallationSystem.sol b/packages/world/src/modules/core/implementations/ModuleInstallationSystem.sol
index 2e55da8b57..7346c304a7 100644
--- a/packages/world/src/modules/core/implementations/ModuleInstallationSystem.sol
+++ b/packages/world/src/modules/core/implementations/ModuleInstallationSystem.sol
@@ -18,9 +18,9 @@ contract ModuleInstallationSystem is System {
   function installModule(IModule module, bytes memory args) public {
     WorldContextProvider.callWithContextOrRevert({
       msgSender: _msgSender(),
+      msgValue: 0,
       target: address(module),
-      funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, args),
-      value: 0
+      funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, args)
     });
 
     // Register the module in the InstalledModules table
diff --git a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol
index 8d1fcfd6e1..4d9c77bc00 100644
--- a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol
+++ b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol
@@ -53,6 +53,7 @@ contract StoreRegistrationSystem is System, IWorldErrors {
       (address systemAddress, ) = Systems.get(ResourceSelector.from(ROOT_NAMESPACE, CORE_SYSTEM_NAME));
       WorldContextProvider.delegatecallWithContextOrRevert({
         msgSender: _msgSender(),
+        msgValue: 0,
         target: systemAddress,
         funcSelectorAndArgs: abi.encodeWithSelector(WorldRegistrationSystem.registerNamespace.selector, namespace)
       });
diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol
index a52090c42f..e0153c7795 100644
--- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol
+++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol
@@ -136,6 +136,7 @@ contract WorldRegistrationSystem is System, IWorldErrors {
    * TODO: instead of mapping to a resource, the function selector could map direcly to a system function,
    * which would save one sload per call, but add some complexity to upgrading systems. TBD.
    * (see https://github.com/latticexyz/mud/issues/444)
+   * TODO: replace separate systemFunctionName and systemFunctionArguments with a signature argument
    */
   function registerFunctionSelector(
     bytes32 resourceSelector,
diff --git a/packages/world/src/modules/core/tables/Balances.sol b/packages/world/src/modules/core/tables/Balances.sol
new file mode 100644
index 0000000000..2242c6a1ad
--- /dev/null
+++ b/packages/world/src/modules/core/tables/Balances.sol
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+/* Autogenerated file. Do not edit manually. */
+
+// Import schema type
+import { SchemaType } from "@latticexyz/schema-type/src/solidity/SchemaType.sol";
+
+// 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 { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol";
+import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol";
+
+bytes32 constant _tableId = bytes32(abi.encodePacked(bytes16(""), bytes16("Balances")));
+bytes32 constant BalancesTableId = _tableId;
+
+library Balances {
+  /** Get the table's key schema */
+  function getKeySchema() internal pure returns (Schema) {
+    SchemaType[] memory _schema = new SchemaType[](1);
+    _schema[0] = SchemaType.BYTES16;
+
+    return SchemaLib.encode(_schema);
+  }
+
+  /** Get the table's value schema */
+  function getValueSchema() internal pure returns (Schema) {
+    SchemaType[] memory _schema = new SchemaType[](1);
+    _schema[0] = SchemaType.UINT256;
+
+    return SchemaLib.encode(_schema);
+  }
+
+  /** Get the table's key names */
+  function getKeyNames() internal pure returns (string[] memory keyNames) {
+    keyNames = new string[](1);
+    keyNames[0] = "namespace";
+  }
+
+  /** Get the table's field names */
+  function getFieldNames() internal pure returns (string[] memory fieldNames) {
+    fieldNames = new string[](1);
+    fieldNames[0] = "balance";
+  }
+
+  /** Register the table's key schema, value schema, key names and value names */
+  function register() internal {
+    StoreSwitch.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames());
+  }
+
+  /** Register the table's key schema, value schema, key names and value names (using the specified store) */
+  function register(IStore _store) internal {
+    _store.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames());
+  }
+
+  /** Get balance */
+  function get(bytes16 namespace) internal view returns (uint256 balance) {
+    bytes32[] memory _keyTuple = new bytes32[](1);
+    _keyTuple[0] = bytes32(namespace);
+
+    bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema());
+    return (uint256(Bytes.slice32(_blob, 0)));
+  }
+
+  /** Get balance (using the specified store) */
+  function get(IStore _store, bytes16 namespace) internal view returns (uint256 balance) {
+    bytes32[] memory _keyTuple = new bytes32[](1);
+    _keyTuple[0] = bytes32(namespace);
+
+    bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema());
+    return (uint256(Bytes.slice32(_blob, 0)));
+  }
+
+  /** Set balance */
+  function set(bytes16 namespace, uint256 balance) internal {
+    bytes32[] memory _keyTuple = new bytes32[](1);
+    _keyTuple[0] = bytes32(namespace);
+
+    StoreSwitch.setField(_tableId, _keyTuple, 0, abi.encodePacked((balance)), getValueSchema());
+  }
+
+  /** Set balance (using the specified store) */
+  function set(IStore _store, bytes16 namespace, uint256 balance) internal {
+    bytes32[] memory _keyTuple = new bytes32[](1);
+    _keyTuple[0] = bytes32(namespace);
+
+    _store.setField(_tableId, _keyTuple, 0, abi.encodePacked((balance)), getValueSchema());
+  }
+
+  /** Tightly pack full data using this table's schema */
+  function encode(uint256 balance) internal pure returns (bytes memory) {
+    return abi.encodePacked(balance);
+  }
+
+  /** Encode keys as a bytes32 array using this table's schema */
+  function encodeKeyTuple(bytes16 namespace) internal pure returns (bytes32[] memory) {
+    bytes32[] memory _keyTuple = new bytes32[](1);
+    _keyTuple[0] = bytes32(namespace);
+
+    return _keyTuple;
+  }
+
+  /* Delete all data for given keys */
+  function deleteRecord(bytes16 namespace) internal {
+    bytes32[] memory _keyTuple = new bytes32[](1);
+    _keyTuple[0] = bytes32(namespace);
+
+    StoreSwitch.deleteRecord(_tableId, _keyTuple, getValueSchema());
+  }
+
+  /* Delete all data for given keys (using the specified store) */
+  function deleteRecord(IStore _store, bytes16 namespace) internal {
+    bytes32[] memory _keyTuple = new bytes32[](1);
+    _keyTuple[0] = bytes32(namespace);
+
+    _store.deleteRecord(_tableId, _keyTuple, getValueSchema());
+  }
+}
diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol
index e82df66d02..1be65a4342 100644
--- a/packages/world/test/World.t.sol
+++ b/packages/world/test/World.t.sol
@@ -24,6 +24,7 @@ import { ResourceSelector } from "../src/ResourceSelector.sol";
 import { ROOT_NAMESPACE, ROOT_NAME, UNLIMITED_DELEGATION } from "../src/constants.sol";
 import { Resource } from "../src/Types.sol";
 import { SystemHookLib } from "../src/SystemHook.sol";
+import { WorldContextProvider } from "../src/WorldContext.sol";
 
 import { NamespaceOwner, NamespaceOwnerTableId } from "../src/tables/NamespaceOwner.sol";
 import { ResourceAccess } from "../src/tables/ResourceAccess.sol";
@@ -630,7 +631,11 @@ contract WorldTest is Test, GasReporter {
       abi.encodeWithSelector(
         WorldTestSystem.delegateCallSubSystem.selector, // Function in system
         address(subSystem), // Address of subsystem
-        abi.encodePacked(WorldTestSystem.msgSender.selector, address(this)) // Function in subsystem
+        WorldContextProvider.appendContext({
+          funcSelectorAndArgs: abi.encodeWithSelector(WorldTestSystem.msgSender.selector),
+          msgSender: address(this),
+          msgValue: uint256(0)
+        })
       )
     );
 
@@ -1154,12 +1159,12 @@ contract WorldTest is Test, GasReporter {
     );
     assertTrue(success, "transfer should succeed");
     assertEq(alice.balance, 0.5 ether, "alice should have 0.5 ether");
-    assertEq(address(world).balance, 0 ether, "world should still have 0 ether");
-    assertEq(address(system).balance, 0.5 ether, "system should have 0.5 ether");
+    assertEq(address(world).balance, 0.5 ether, "world should have 0.5 ether");
+    assertEq(address(system).balance, 0 ether, "system should have 0 ether");
   }
 
   function testNonPayableSystem() public {
-    // Register a root system with a non-payable function in the world
+    // Register a non-root system with a non-payable function in the world
     WorldTestSystem system = new WorldTestSystem();
     bytes16 namespace = "noroot";
     bytes16 name = "testSystem";
@@ -1184,16 +1189,17 @@ contract WorldTest is Test, GasReporter {
     (bool success, ) = address(world).call{ value: 0.5 ether }(
       abi.encodeWithSelector(WorldTestSystem.msgSender.selector)
     );
-    assertFalse(success, "transfer should fail");
-    assertEq(alice.balance, 1 ether, "alice should have 1 ether");
-    assertEq(address(world).balance, 0 ether, "world should have 0 ether");
+    // The call should succeed because the value is not forwarded to the system
+    assertTrue(success, "transfer should succeed");
+    assertEq(alice.balance, 0.5 ether, "alice should have 0.5 ether");
+    assertEq(address(world).balance, 0.5 ether, "world should have 0.5 ether");
     assertEq(address(system).balance, 0 ether, "system should have 0 ether");
   }
 
   function testNonPayableFallbackSystem() public {
     // Register a root system with a non-payable function in the world
     WorldTestSystem system = new WorldTestSystem();
-    bytes16 namespace = "noroot";
+    bytes16 namespace = ROOT_NAMESPACE;
     bytes16 name = "testSystem";
     bytes32 resourceSelector = ResourceSelector.from(namespace, name);
     world.registerSystem(resourceSelector, system, true);
@@ -1221,9 +1227,9 @@ contract WorldTest is Test, GasReporter {
   }
 
   function testPayableFallbackSystem() public {
-    // Register a root system with a non-payable function in the world
+    // Register a root system with a payable function in the world
     PayableFallbackSystem system = new PayableFallbackSystem();
-    bytes16 namespace = "noroot";
+    bytes16 namespace = ROOT_NAMESPACE;
     bytes16 name = "testSystem";
     bytes32 resourceSelector = ResourceSelector.from(namespace, name);
     world.registerSystem(resourceSelector, system, true);
@@ -1244,16 +1250,16 @@ contract WorldTest is Test, GasReporter {
 
     // Send 0.5 eth to the system's fallback function (non-payable) via the World
     (bool success, ) = address(world).call{ value: 0.5 ether }(abi.encodeWithSignature("systemFallback()"));
-    assertTrue(success, "transfer should fail");
+    assertTrue(success, "transfer should succeed");
     assertEq(alice.balance, 0.5 ether, "alice should have 0.5 ether");
-    assertEq(address(world).balance, 0 ether, "world should have 0 ether");
-    assertEq(address(system).balance, 0.5 ether, "system should have 0.5 ether");
+    assertEq(address(world).balance, 0.5 ether, "world should have 0.5 ether");
+    assertEq(address(system).balance, 0 ether, "system should have 0 ether");
   }
 
   function testPayableRootSystem() public {
     // Register a root system with a payable function in the world
     WorldTestSystem system = new WorldTestSystem();
-    bytes16 namespace = "";
+    bytes16 namespace = ROOT_NAMESPACE;
     bytes16 name = "testSystem";
     bytes32 resourceSelector = ResourceSelector.from(namespace, name);
     world.registerSystem(resourceSelector, system, true);
@@ -1279,7 +1285,7 @@ contract WorldTest is Test, GasReporter {
     assertTrue(success, "transfer should succeed");
     assertEq(alice.balance, 0.5 ether, "alice should have 0.5 ether");
     assertEq(address(world).balance, 0.5 ether, "world should have 0.5 ether");
-    assertEq(address(system).balance, 0 ether, "system should have 0 ether (bc it was delegatecalled)");
+    assertEq(address(system).balance, 0 ether, "system should have 0 ether");
   }
 
   // TODO: add a test for systems writing to tables via the World
diff --git a/packages/world/test/WorldBalance.t.sol b/packages/world/test/WorldBalance.t.sol
new file mode 100644
index 0000000000..50c46aeb88
--- /dev/null
+++ b/packages/world/test/WorldBalance.t.sol
@@ -0,0 +1,339 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+import "forge-std/Test.sol";
+import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol";
+import { World } from "../src/World.sol";
+import { System } from "../src/System.sol";
+import { IBaseWorld } from "../src/interfaces/IBaseWorld.sol";
+import { ResourceSelector } from "../src/ResourceSelector.sol";
+import { ROOT_NAMESPACE } from "../src/constants.sol";
+import { CoreModule } from "../src/modules/core/CoreModule.sol";
+import { Balances } from "../src/modules/core/tables/Balances.sol";
+import { IWorldErrors } from "../src/interfaces/IWorldErrors.sol";
+
+using ResourceSelector for bytes32;
+
+contract WorldBalanceTestSystem is System {
+  function echoValue() public payable returns (uint256) {
+    return _msgValue();
+  }
+}
+
+contract WorldBalanceTest is Test, GasReporter {
+  IBaseWorld public world;
+  WorldBalanceTestSystem public rootSystem = new WorldBalanceTestSystem();
+  WorldBalanceTestSystem public nonRootSystem = new WorldBalanceTestSystem();
+  bytes16 public namespace = "namespace";
+  bytes32 public rootSystemId = ResourceSelector.from(ROOT_NAMESPACE, "testSystem");
+  bytes32 public nonRootSystemId = ResourceSelector.from(namespace, "testSystem");
+  address public caller = address(4242);
+
+  function setUp() public {
+    world = IBaseWorld(address(new World()));
+    world.installRootModule(new CoreModule(), new bytes(0));
+    world.registerSystem(rootSystemId, rootSystem, true);
+    world.registerSystem(nonRootSystemId, nonRootSystem, true);
+
+    world.registerRootFunctionSelector(rootSystemId, rootSystem.echoValue.selector, rootSystem.echoValue.selector);
+    world.registerFunctionSelector(nonRootSystemId, "echoValue", "()");
+  }
+
+  function testCallWithValue() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Call a function on the root system with value via call
+    vm.deal(caller, value);
+    vm.prank(caller);
+    bytes memory data = world.call{ value: value }(rootSystemId, abi.encodeCall(rootSystem.echoValue, ()));
+    assertEq(abi.decode(data, (uint256)), value);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Call a function on a non-root system with value via call
+    vm.deal(caller, value);
+    vm.prank(caller);
+    data = world.call{ value: value }(nonRootSystemId, abi.encodeCall(rootSystem.echoValue, ()));
+    assertEq(abi.decode(data, (uint256)), value);
+
+    // Expect the non root namespace to have the value as balance
+    assertEq(Balances.get(world, namespace), value);
+
+    // Expect the root namespace to still have the same balance as before
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+  }
+
+  function testCallFromWithValue() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Call a function on the root system with value via callFrom
+    vm.deal(caller, value);
+    vm.prank(caller);
+    bytes memory data = world.callFrom{ value: value }(caller, rootSystemId, abi.encodeCall(rootSystem.echoValue, ()));
+    assertEq(abi.decode(data, (uint256)), value);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Call a function on a non-root system with value via callFrom
+    vm.deal(caller, value);
+    vm.prank(caller);
+    data = world.callFrom{ value: value }(caller, nonRootSystemId, abi.encodeCall(rootSystem.echoValue, ()));
+    assertEq(abi.decode(data, (uint256)), value);
+
+    // Expect the non root namespace to have the value as balance
+    assertEq(Balances.get(world, namespace), value);
+
+    // Expect the root namespace to still have the same balance as before
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+  }
+
+  function testFallbackWithValue() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Call a function on the root system with value via the registered root function selector
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (bool success, bytes memory data) = address(world).call{ value: value }(abi.encodeWithSignature("echoValue()"));
+    assertTrue(success);
+    assertEq(abi.decode(data, (uint256)), value);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Call a function on a non-root system with value
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (success, data) = address(world).call{ value: value }(abi.encodeWithSignature("namespace_testSystem_echoValue()"));
+    assertTrue(success);
+    assertEq(abi.decode(data, (uint256)), value);
+
+    // Expect the non root namespace to have the value as balance
+    assertEq(Balances.get(world, namespace), value);
+
+    // Expect the root namespace to still have the same balance as before
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+  }
+
+  function testReceiveWithValue() public {
+    uint256 value = 1 ether;
+
+    // Expect the root to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+
+    // Send value to the world without calldata
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (bool success, bytes memory data) = address(world).call{ value: value }("");
+    assertTrue(success);
+    assertEq(data.length, 0);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+  }
+
+  function testTransferBalanceToNamespace() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Send balance to root namespace
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (bool success, bytes memory data) = address(world).call{ value: value }("");
+    assertTrue(success);
+    assertEq(data.length, 0);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Transfer the balance to another namespace
+    world.transferBalanceToNamespace(ROOT_NAMESPACE, namespace, value);
+
+    // Expect the root namespace to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+
+    // Expect the non root namespace to have the value as balance
+    assertEq(Balances.get(world, namespace), value);
+  }
+
+  function testTransferBalanceToNamespaceRevertInsufficientBalance() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Send balance to root namespace
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (bool success, bytes memory data) = address(world).call{ value: value }("");
+    assertTrue(success);
+    assertEq(data.length, 0);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect revert when attempting to transfer more than the balance
+    vm.expectRevert(abi.encodeWithSelector(IWorldErrors.InsufficientBalance.selector, value, value + 1));
+    world.transferBalanceToNamespace(ROOT_NAMESPACE, namespace, value + 1);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect the non root namespace to have no balance
+    assertEq(Balances.get(world, namespace), 0);
+  }
+
+  function testTransferBalanceToNamespaceRevertAccessDenied() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Send balance to root namespace
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (bool success, bytes memory data) = address(world).call{ value: value }("");
+    assertTrue(success);
+    assertEq(data.length, 0);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect revert when attempting to transfer from a namespace without access
+    vm.prank(caller);
+    vm.expectRevert(
+      abi.encodeWithSelector(
+        IWorldErrors.AccessDenied.selector,
+        ResourceSelector.from(ROOT_NAMESPACE).toString(),
+        caller
+      )
+    );
+    world.transferBalanceToNamespace(ROOT_NAMESPACE, namespace, value);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect the non root namespace to have no balance
+    assertEq(Balances.get(world, namespace), 0);
+  }
+
+  function testTransferBalanceToAddress() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Send balance to root namespace
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (bool success, bytes memory data) = address(world).call{ value: value }("");
+    assertTrue(success);
+    assertEq(data.length, 0);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect the receiver to not have any balance
+    address receiver = address(1234);
+    assertEq(receiver.balance, 0);
+
+    // Transfer the balance to the receiver
+    world.transferBalanceToAddress(ROOT_NAMESPACE, receiver, value);
+
+    // Expect the root namespace to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+
+    // Expect the receiver to have value as balance
+    assertEq(receiver.balance, value);
+  }
+
+  function testTransferBalanceToAddressRevertInsufficientBalance() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Send balance to root namespace
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (bool success, bytes memory data) = address(world).call{ value: value }("");
+    assertTrue(success);
+    assertEq(data.length, 0);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect the receiver to not have any balance
+    address receiver = address(1234);
+    assertEq(receiver.balance, 0);
+
+    // Expect revert when attempting to transfer more than the balance
+    vm.expectRevert(abi.encodeWithSelector(IWorldErrors.InsufficientBalance.selector, value, value + 1));
+    world.transferBalanceToAddress(ROOT_NAMESPACE, receiver, value + 1);
+
+    // Expect the root namespace to have value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect the receiver to have no balance
+    assertEq(receiver.balance, 0);
+  }
+
+  function testTransferBalanceToAddressRevertAccessDenied() public {
+    uint256 value = 1 ether;
+
+    // Expect the root and non root namespaces to have no balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), 0);
+    assertEq(Balances.get(world, namespace), 0);
+
+    // Send balance to root namespace
+    vm.deal(caller, value);
+    vm.prank(caller);
+    (bool success, bytes memory data) = address(world).call{ value: value }("");
+    assertTrue(success);
+    assertEq(data.length, 0);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect the receiver to not have any balance
+    address receiver = address(1234);
+    assertEq(receiver.balance, 0);
+
+    // Expect revert when attempting to transfer from a namespace without access
+    vm.prank(caller);
+    vm.expectRevert(
+      abi.encodeWithSelector(
+        IWorldErrors.AccessDenied.selector,
+        ResourceSelector.from(ROOT_NAMESPACE).toString(),
+        caller
+      )
+    );
+    world.transferBalanceToAddress(ROOT_NAMESPACE, receiver, value);
+
+    // Expect the root namespace to have the value as balance
+    assertEq(Balances.get(world, ROOT_NAMESPACE), value);
+
+    // Expect the receiver to have no balance
+    assertEq(receiver.balance, 0);
+  }
+}
diff --git a/packages/world/test/WorldContext.t.sol b/packages/world/test/WorldContext.t.sol
new file mode 100644
index 0000000000..4ba6fb2454
--- /dev/null
+++ b/packages/world/test/WorldContext.t.sol
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+import "forge-std/Test.sol";
+import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol";
+import { WorldContextProvider, WorldContextConsumer } from "../src/WorldContext.sol";
+
+contract TestContextConsumer is WorldContextConsumer {
+  event Context(bytes args, address msgSender, uint256 msgValue);
+
+  function emitContext(bytes memory someArgument) public {
+    emit Context(someArgument, _msgSender(), _msgValue());
+  }
+}
+
+contract WorldContextTest is Test, GasReporter {
+  event Context(bytes args, address msgSender, uint256 msgValue);
+
+  TestContextConsumer public consumer = new TestContextConsumer();
+
+  function testFuzzAppendContext(bytes memory funcSelectorAndArgs, address msgSender, uint256 msgValue) public {
+    assertEq(
+      keccak256(abi.encodePacked(funcSelectorAndArgs, msgSender, msgValue)),
+      keccak256(WorldContextProvider.appendContext(funcSelectorAndArgs, msgSender, msgValue))
+    );
+  }
+
+  function testFuzzCallExtractContext(bytes memory args, address msgSender, uint256 msgValue) public {
+    vm.assume(msgSender != address(0));
+
+    vm.expectEmit(true, true, true, true);
+    emit Context(args, msgSender, msgValue);
+    WorldContextProvider.callWithContextOrRevert({
+      msgSender: msgSender,
+      msgValue: msgValue,
+      target: address(consumer),
+      funcSelectorAndArgs: abi.encodeWithSelector(TestContextConsumer.emitContext.selector, args)
+    });
+  }
+
+  function testFuzzDelegatecallExtractContext(bytes memory args, address msgSender, uint256 msgValue) public {
+    vm.assume(msgSender != address(0));
+
+    vm.expectEmit(true, true, true, true);
+    emit Context(args, msgSender, msgValue);
+    WorldContextProvider.delegatecallWithContextOrRevert({
+      msgSender: msgSender,
+      msgValue: msgValue,
+      target: address(consumer),
+      funcSelectorAndArgs: abi.encodeWithSelector(TestContextConsumer.emitContext.selector, args)
+    });
+  }
+}