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) + }); + } +}