diff --git a/packages/world-module-erc20-own-store/.changeset/README.md b/packages/world-module-erc20-own-store/.changeset/README.md new file mode 100644 index 0000000000..e5b6d8d6a6 --- /dev/null +++ b/packages/world-module-erc20-own-store/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/packages/world-module-erc20-own-store/.changeset/config.json b/packages/world-module-erc20-own-store/.changeset/config.json new file mode 100644 index 0000000000..ae82eba8fb --- /dev/null +++ b/packages/world-module-erc20-own-store/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/packages/world-module-erc20-own-store/.changeset/thirty-wasps-flow.md b/packages/world-module-erc20-own-store/.changeset/thirty-wasps-flow.md new file mode 100644 index 0000000000..5639520290 --- /dev/null +++ b/packages/world-module-erc20-own-store/.changeset/thirty-wasps-flow.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/world-module-erc20-own-store": major +--- + +Implement ERC20 Module with own Store diff --git a/packages/world-module-erc20-own-store/.gitignore b/packages/world-module-erc20-own-store/.gitignore new file mode 100644 index 0000000000..764d7db934 --- /dev/null +++ b/packages/world-module-erc20-own-store/.gitignore @@ -0,0 +1,3 @@ +cache +out +src/codegen/world/ diff --git a/packages/world-module-erc20-own-store/.solhint.json b/packages/world-module-erc20-own-store/.solhint.json new file mode 100644 index 0000000000..4e2baa8be7 --- /dev/null +++ b/packages/world-module-erc20-own-store/.solhint.json @@ -0,0 +1,8 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error", ">=0.8.0"], + "avoid-low-level-calls": "off", + "func-visibility": ["warn", { "ignoreConstructors": true }] + } +} diff --git a/packages/world-module-erc20-own-store/README.md b/packages/world-module-erc20-own-store/README.md new file mode 100644 index 0000000000..232bc049c9 --- /dev/null +++ b/packages/world-module-erc20-own-store/README.md @@ -0,0 +1,5 @@ +# Standalone ERC20 Module with own Store + +This is an `ERC20` token template that is compatible with the MUD framework. The logic of the token contract is near native and resides within the token contract, while its storage makes use of `Store` and is reflected in corresponding tables. + +The `OpenZeppelin` implementation of ERC20 is used as reference but tests are written in `Solidity`. diff --git a/packages/world-module-erc20-own-store/foundry.toml b/packages/world-module-erc20-own-store/foundry.toml new file mode 100644 index 0000000000..f0e017f5a0 --- /dev/null +++ b/packages/world-module-erc20-own-store/foundry.toml @@ -0,0 +1,15 @@ +[profile.default] +solc = "0.8.24" +ffi = false +fuzz_runs = 256 +optimizer = true +optimizer_runs = 3000 +verbosity = 2 +allow_paths = ["../../node_modules", "../"] +src = "src" +out = "out" +bytecode_hash = "none" +extra_output_files = [ + "abi", + "evm.bytecode" +] diff --git a/packages/world-module-erc20-own-store/gas-report.json b/packages/world-module-erc20-own-store/gas-report.json new file mode 100644 index 0000000000..57e1456bbf --- /dev/null +++ b/packages/world-module-erc20-own-store/gas-report.json @@ -0,0 +1,170 @@ +[ + { + "file": "test/ERC20.t.sol", + "test": "testApprove", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testApprove", + "name": "token approve", + "gasUsed": 64807 + }, + { + "file": "test/ERC20.t.sol", + "test": "testBalanceOf", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testBalanceOf", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testBalanceOf", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testBalanceOf", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testBalanceOf", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenBurn", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenBurn", + "name": "token burn", + "gasUsed": 63648 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenBurnInvalidCaller", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenBurnInvalidCaller", + "name": "token burn invalid caller", + "gasUsed": 32388 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenBurnZeroAddress", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenBurnZeroAddress", + "name": "token burn zero address", + "gasUsed": 32309 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenMint", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenMint", + "name": "token mint", + "gasUsed": 85313 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenMintInvalidCaller", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenMintInvalidCaller", + "name": "token mint invalid caller", + "gasUsed": 32410 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenMintToZeroAddress", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenMintToZeroAddress", + "name": "token mint to zero address", + "gasUsed": 32331 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenSetUp", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenTransfer", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenTransfer", + "name": "token transfer", + "gasUsed": 74442 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenTransferInsufficientBalance", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTokenTransferInsufficientBalance", + "name": "token transfer insufficient balance", + "gasUsed": 30032 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTransferFrom", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTransferFrom", + "name": "token transferFrom", + "gasUsed": 86297 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTransferFromInsufficientAllowance", + "name": "token constructor", + "gasUsed": 7686226 + }, + { + "file": "test/ERC20.t.sol", + "test": "testTransferFromInsufficientAllowance", + "name": "token transferFrom insufficient allowance", + "gasUsed": 32482 + } +] diff --git a/packages/world-module-erc20-own-store/mud.config.ts b/packages/world-module-erc20-own-store/mud.config.ts new file mode 100644 index 0000000000..402f4dce6c --- /dev/null +++ b/packages/world-module-erc20-own-store/mud.config.ts @@ -0,0 +1,32 @@ +import { defineWorld } from "@latticexyz/world"; + +export default defineWorld({ + namespace: "erc20-store", + tables: { + Token: { + schema: { + decimals: "uint8", + totalSupply: "uint256", + owner: "address", + name: "string", + symbol: "string", + }, + key: [], // Singleton table + }, + Balances: { + schema: { + account: "address", + balance: "uint256", + }, + key: ["account"], + }, + Allowances: { + schema: { + account: "address", + spender: "address", + approval: "uint256", + }, + key: ["account", "spender"], + }, + }, +}); diff --git a/packages/world-module-erc20-own-store/package.json b/packages/world-module-erc20-own-store/package.json new file mode 100644 index 0000000000..5ece65855f --- /dev/null +++ b/packages/world-module-erc20-own-store/package.json @@ -0,0 +1,56 @@ +{ + "name": "@latticexyz/world-module-erc20-own-store", + "version": "2.1.1", + "description": "ERC20 World Module with Own Store", + "repository": { + "type": "git", + "url": "https://github.com/latticexyz/mud.git", + "directory": "packages/world-module-erc20-own-store" + }, + "license": "MIT", + "type": "module", + "exports": { + "./mud.config": "./dist/mud.config.js", + "./out/*": "./out/*" + }, + "typesVersions": { + "*": { + "mud.config": [ + "./dist/mud.config.d.ts" + ] + } + }, + "files": [ + "dist", + "out", + "src" + ], + "scripts": { + "build": "mud build", + "clean": "forge clean && shx rm -rf src/**/codegen", + "gas-report": "gas-report --save gas-report.json", + "lint": "solhint --config ./.solhint.json 'src/**/*.sol'", + "test": "forge test", + "test:ci": "pnpm run test" + }, + "dependencies": { + "@latticexyz/schema-type": "workspace:*", + "@latticexyz/store": "workspace:*", + "@latticexyz/world": "workspace:*" + }, + "devDependencies": { + "@latticexyz/abi-ts": "workspace:*", + "@latticexyz/cli": "workspace:*", + "@latticexyz/gas-report": "workspace:*", + "@types/node": "^18.15.11", + "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", + "forge-std": "https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1", + "solhint": "^3.3.7", + "tsup": "^6.7.0", + "tsx": "^3.12.6", + "vitest": "0.34.6" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/world-module-erc20-own-store/remappings.txt b/packages/world-module-erc20-own-store/remappings.txt new file mode 100644 index 0000000000..66be45ecbd --- /dev/null +++ b/packages/world-module-erc20-own-store/remappings.txt @@ -0,0 +1,3 @@ +ds-test/=node_modules/ds-test/src/ +forge-std/=node_modules/forge-std/src/ +@latticexyz/=node_modules/@latticexyz/ \ No newline at end of file diff --git a/packages/world-module-erc20-own-store/src/ERC20.sol b/packages/world-module-erc20-own-store/src/ERC20.sol new file mode 100644 index 0000000000..3f444e2cec --- /dev/null +++ b/packages/world-module-erc20-own-store/src/ERC20.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { IERC20Errors } from "./IERC20Errors.sol"; +import { IERC20Events } from "./IERC20Events.sol"; +import { Token } from "./codegen/tables/Token.sol"; +import { Balances } from "./codegen/tables/Balances.sol"; +import { Allowances } from "./codegen/tables/Allowances.sol"; + +import { Store } from "@latticexyz/store/src/Store.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { IStoreHook } from "@latticexyz/store/src/IStoreHook.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { EncodedLengths } from "@latticexyz/store/src/EncodedLengths.sol"; + +/** + * @title ERC20 Module with own Store + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev Implementation of EIP-20 that has on instance of `Store` which enables built in indexing and storage packing. + */ +contract ERC20 is Store, IERC20Errors, IERC20Events { + constructor(string memory _name, string memory _symbol, address _owner, uint8 _decimals) { + StoreCore.registerInternalTables(); + + Token.register(); + Balances.register(); + Allowances.register(); + + Token.set(_decimals, 0, _owner, _name, _symbol); + } + + /** + * @dev Returns the name of the token. + * @return The name of the token. + */ + function name() public view returns (string memory) { + return Token.getName(); + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + * @return The symbol of the token. + */ + function symbol() public view returns (string memory) { + return Token.getSymbol(); + } + + /** + * @dev Returns the decimals of the token. + * @return The number of decimals of the token. + */ + function decimals() public view returns (uint8) { + return Token.getDecimals(); + } + + /** + * @dev Returns the total supply of the token. + * @return The total supply of the token. + */ + function totalSupply() public view returns (uint256) { + return Token.getTotalSupply(); + } + + /** + * @dev Returns the balance of the `account`. + * @param account The address of the account to get the balance of. + * @return The balance of the `account`. + */ + function balanceOf(address account) public view returns (uint256) { + return Balances.get(account); + } + + /** + * @dev Returns the allowance of `spender` for `owner`. + * @param owner The address of the owner. + * @param spender The address of the spender. + */ + function allowance(address owner, address spender) public view returns (uint256) { + return Allowances.get(owner, spender); + } + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) public returns (bool) { + _approve(msg.sender, spender, value); + return true; + } + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) public returns (bool) { + _transfer(msg.sender, to, value); + return true; + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) public returns (bool) { + _spendAllowance(from, msg.sender, value); + _transfer(from, to, value); + return true; + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + */ + function mint(address account, uint256 value) external { + assert(Token.getOwner() == msg.sender); + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + */ + function burn(address account, uint256 value) external { + assert(Token.getOwner() == msg.sender); + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + _update(account, address(0), value); + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + uint256 supplyBefore = Token.getTotalSupply(); + Token.setTotalSupply(supplyBefore + value); + } else { + uint256 fromBalancePrior = Balances.get(from); + if (fromBalancePrior < value) { + revert ERC20InsufficientBalance(from, fromBalancePrior, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + Balances.setBalance(from, fromBalancePrior - value); + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + uint256 supplyBefore = Token.getTotalSupply(); + Token.setTotalSupply(supplyBefore - value); + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + uint256 balanceToPrior = Balances.get(to); + Balances.setBalance(to, balanceToPrior + value); + } + } + + emit Transfer(from, to, value); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + * + */ + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 value, bool emitEvent) internal { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + Allowances.setApproval(owner, spender, value); + if (emitEvent) { + emit Approval(owner, spender, value); + } + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal { + uint256 currentAllowance = Allowances.get(owner, spender); + if (currentAllowance != type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value, false); + } + } + } + + /** + * ToDo: Update natspec to match above + */ + + // Set full record (including full dynamic data) + function setRecord( + ResourceId tableId, + bytes32[] calldata keyTuple, + bytes calldata staticData, + EncodedLengths encodedLengths, + bytes calldata dynamicData + ) public { + StoreCore.setRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData); + } + + // Splice data in the static part of the record + function spliceStaticData(ResourceId tableId, bytes32[] calldata keyTuple, uint48 start, bytes calldata data) public { + StoreCore.spliceStaticData(tableId, keyTuple, start, data); + } + + // Splice data in the dynamic part of the record + function spliceDynamicData( + ResourceId tableId, + bytes32[] calldata keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes calldata data + ) public { + StoreCore.spliceDynamicData(tableId, keyTuple, dynamicFieldIndex, startWithinField, deleteCount, data); + } + + // Set partial data at field index + function setField(ResourceId tableId, bytes32[] calldata keyTuple, uint8 fieldIndex, bytes calldata data) public { + StoreCore.setField(tableId, keyTuple, fieldIndex, data); + } + + // Set partial data at field index + function setField( + ResourceId tableId, + bytes32[] calldata keyTuple, + uint8 fieldIndex, + bytes calldata data, + FieldLayout fieldLayout + ) public { + StoreCore.setField(tableId, keyTuple, fieldIndex, data, fieldLayout); + } + + // Set partial data at field index + function setStaticField( + ResourceId tableId, + bytes32[] calldata keyTuple, + uint8 fieldIndex, + bytes calldata data, + FieldLayout fieldLayout + ) public { + StoreCore.setStaticField(tableId, keyTuple, fieldIndex, data, fieldLayout); + } + + // Set partial data at dynamic field index + function setDynamicField( + ResourceId tableId, + bytes32[] calldata keyTuple, + uint8 dynamicFieldIndex, + bytes calldata data + ) public { + StoreCore.setDynamicField(tableId, keyTuple, dynamicFieldIndex, data); + } + + // Push encoded items to the dynamic field at field index + function pushToDynamicField( + ResourceId tableId, + bytes32[] calldata keyTuple, + uint8 dynamicFieldIndex, + bytes calldata dataToPush + ) public { + StoreCore.pushToDynamicField(tableId, keyTuple, dynamicFieldIndex, dataToPush); + } + + // Pop byte length from the dynamic field at field index + function popFromDynamicField( + ResourceId tableId, + bytes32[] calldata keyTuple, + uint8 dynamicFieldIndex, + uint256 byteLengthToPop + ) public { + StoreCore.popFromDynamicField(tableId, keyTuple, dynamicFieldIndex, byteLengthToPop); + } + + // Set full record (including full dynamic data) + function deleteRecord(ResourceId tableId, bytes32[] memory keyTuple) public { + StoreCore.deleteRecord(tableId, keyTuple); + } + + function registerTable( + ResourceId tableId, + FieldLayout fieldLayout, + Schema keySchema, + Schema valueSchema, + string[] calldata keyNames, + string[] calldata fieldNames + ) public { + StoreCore.registerTable(tableId, fieldLayout, keySchema, valueSchema, keyNames, fieldNames); + } + + // Register hook to be called when a record or field is set or deleted + function registerStoreHook(ResourceId tableId, IStoreHook hookAddress, uint8 enabledHooksBitmap) public { + StoreCore.registerStoreHook(tableId, hookAddress, enabledHooksBitmap); + } + + // Unregister hook to be called when a record or field is set or deleted + function unregisterStoreHook(ResourceId tableId, IStoreHook hookAddress) public { + StoreCore.unregisterStoreHook(tableId, hookAddress); + } +} diff --git a/packages/world-module-erc20-own-store/src/IERC20Errors.sol b/packages/world-module-erc20-own-store/src/IERC20Errors.sol new file mode 100644 index 0000000000..869ccc8250 --- /dev/null +++ b/packages/world-module-erc20-own-store/src/IERC20Errors.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} diff --git a/packages/world-module-erc20-own-store/src/IERC20Events.sol b/packages/world-module-erc20-own-store/src/IERC20Events.sol new file mode 100644 index 0000000000..f86a921f3e --- /dev/null +++ b/packages/world-module-erc20-own-store/src/IERC20Events.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +interface IERC20Events { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/packages/world-module-erc20-own-store/src/codegen/index.sol b/packages/world-module-erc20-own-store/src/codegen/index.sol new file mode 100644 index 0000000000..26ea531e76 --- /dev/null +++ b/packages/world-module-erc20-own-store/src/codegen/index.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { Token, TokenData } from "./tables/Token.sol"; +import { Balances } from "./tables/Balances.sol"; +import { Allowances } from "./tables/Allowances.sol"; diff --git a/packages/world-module-erc20-own-store/src/codegen/tables/Allowances.sol b/packages/world-module-erc20-own-store/src/codegen/tables/Allowances.sol new file mode 100644 index 0000000000..463132cd2d --- /dev/null +++ b/packages/world-module-erc20-own-store/src/codegen/tables/Allowances.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// 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 { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +library Allowances { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "erc20-store", name: "Allowances", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x746265726332302d73746f7265000000416c6c6f77616e636573000000000000); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of (address, address) + Schema constant _keySchema = Schema.wrap(0x0028020061610000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (uint256) + Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](2); + keyNames[0] = "account"; + keyNames[1] = "spender"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "approval"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get approval. + */ + function getApproval(address account, address spender) internal view returns (uint256 approval) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get approval. + */ + function _getApproval(address account, address spender) internal view returns (uint256 approval) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get approval. + */ + function get(address account, address spender) internal view returns (uint256 approval) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get approval. + */ + function _get(address account, address spender) internal view returns (uint256 approval) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set approval. + */ + function setApproval(address account, address spender, uint256 approval) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((approval)), _fieldLayout); + } + + /** + * @notice Set approval. + */ + function _setApproval(address account, address spender, uint256 approval) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((approval)), _fieldLayout); + } + + /** + * @notice Set approval. + */ + function set(address account, address spender, uint256 approval) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((approval)), _fieldLayout); + } + + /** + * @notice Set approval. + */ + function _set(address account, address spender, uint256 approval) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((approval)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(address account, address spender) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(address account, address spender) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(uint256 approval) internal pure returns (bytes memory) { + return abi.encodePacked(approval); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode(uint256 approval) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData = encodeStatic(approval); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(address account, address spender) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + return _keyTuple; + } +} diff --git a/packages/world-module-erc20-own-store/src/codegen/tables/Balances.sol b/packages/world-module-erc20-own-store/src/codegen/tables/Balances.sol new file mode 100644 index 0000000000..839a3573d6 --- /dev/null +++ b/packages/world-module-erc20-own-store/src/codegen/tables/Balances.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// 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 { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +library Balances { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "erc20-store", name: "Balances", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x746265726332302d73746f726500000042616c616e6365730000000000000000); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of (address) + Schema constant _keySchema = Schema.wrap(0x0014010061000000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (uint256) + Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](1); + keyNames[0] = "account"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "balance"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get balance. + */ + function getBalance(address account) internal view returns (uint256 balance) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get balance. + */ + function _getBalance(address account) internal view returns (uint256 balance) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get balance. + */ + function get(address account) internal view returns (uint256 balance) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get balance. + */ + function _get(address account) internal view returns (uint256 balance) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set balance. + */ + function setBalance(address account, uint256 balance) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((balance)), _fieldLayout); + } + + /** + * @notice Set balance. + */ + function _setBalance(address account, uint256 balance) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((balance)), _fieldLayout); + } + + /** + * @notice Set balance. + */ + function set(address account, uint256 balance) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((balance)), _fieldLayout); + } + + /** + * @notice Set balance. + */ + function _set(address account, uint256 balance) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((balance)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(address account) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(address account) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(uint256 balance) internal pure returns (bytes memory) { + return abi.encodePacked(balance); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode(uint256 balance) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData = encodeStatic(balance); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(address account) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + return _keyTuple; + } +} diff --git a/packages/world-module-erc20-own-store/src/codegen/tables/Token.sol b/packages/world-module-erc20-own-store/src/codegen/tables/Token.sol new file mode 100644 index 0000000000..6ea7921229 --- /dev/null +++ b/packages/world-module-erc20-own-store/src/codegen/tables/Token.sol @@ -0,0 +1,693 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// 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 { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +struct TokenData { + uint8 decimals; + uint256 totalSupply; + address owner; + string name; + string symbol; +} + +library Token { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "erc20-store", name: "Token", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x746265726332302d73746f7265000000546f6b656e0000000000000000000000); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0035030201201400000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of () + Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (uint8, uint256, address, string, string) + Schema constant _valueSchema = Schema.wrap(0x00350302001f61c5c50000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](0); + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](5); + fieldNames[0] = "decimals"; + fieldNames[1] = "totalSupply"; + fieldNames[2] = "owner"; + fieldNames[3] = "name"; + fieldNames[4] = "symbol"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get decimals. + */ + function getDecimals() internal view returns (uint8 decimals) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint8(bytes1(_blob))); + } + + /** + * @notice Get decimals. + */ + function _getDecimals() internal view returns (uint8 decimals) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint8(bytes1(_blob))); + } + + /** + * @notice Set decimals. + */ + function setDecimals(uint8 decimals) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((decimals)), _fieldLayout); + } + + /** + * @notice Set decimals. + */ + function _setDecimals(uint8 decimals) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((decimals)), _fieldLayout); + } + + /** + * @notice Get totalSupply. + */ + function getTotalSupply() internal view returns (uint256 totalSupply) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 1, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get totalSupply. + */ + function _getTotalSupply() internal view returns (uint256 totalSupply) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 1, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set totalSupply. + */ + function setTotalSupply(uint256 totalSupply) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 1, abi.encodePacked((totalSupply)), _fieldLayout); + } + + /** + * @notice Set totalSupply. + */ + function _setTotalSupply(uint256 totalSupply) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 1, abi.encodePacked((totalSupply)), _fieldLayout); + } + + /** + * @notice Get owner. + */ + function getOwner() internal view returns (address owner) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 2, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get owner. + */ + function _getOwner() internal view returns (address owner) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 2, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Set owner. + */ + function setOwner(address owner) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 2, abi.encodePacked((owner)), _fieldLayout); + } + + /** + * @notice Set owner. + */ + function _setOwner(address owner) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 2, abi.encodePacked((owner)), _fieldLayout); + } + + /** + * @notice Get name. + */ + function getName() internal view returns (string memory name) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 0); + return (string(_blob)); + } + + /** + * @notice Get name. + */ + function _getName() internal view returns (string memory name) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 0); + return (string(_blob)); + } + + /** + * @notice Set name. + */ + function setName(string memory name) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 0, bytes((name))); + } + + /** + * @notice Set name. + */ + function _setName(string memory name) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setDynamicField(_tableId, _keyTuple, 0, bytes((name))); + } + + /** + * @notice Get the length of name. + */ + function lengthName() internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of name. + */ + function _lengthName() internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get an item of name. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItemName(uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Get an item of name. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItemName(uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Push a slice to name. + */ + function pushName(string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Push a slice to name. + */ + function _pushName(string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Pop a slice from name. + */ + function popName() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Pop a slice from name. + */ + function _popName() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Update a slice of name at `_index`. + */ + function updateName(uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of name at `_index`. + */ + function _updateName(uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Get symbol. + */ + function getSymbol() internal view returns (string memory symbol) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 1); + return (string(_blob)); + } + + /** + * @notice Get symbol. + */ + function _getSymbol() internal view returns (string memory symbol) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 1); + return (string(_blob)); + } + + /** + * @notice Set symbol. + */ + function setSymbol(string memory symbol) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 1, bytes((symbol))); + } + + /** + * @notice Set symbol. + */ + function _setSymbol(string memory symbol) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setDynamicField(_tableId, _keyTuple, 1, bytes((symbol))); + } + + /** + * @notice Get the length of symbol. + */ + function lengthSymbol() internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 1); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of symbol. + */ + function _lengthSymbol() internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 1); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get an item of symbol. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItemSymbol(uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 1, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Get an item of symbol. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItemSymbol(uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 1, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Push a slice to symbol. + */ + function pushSymbol(string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 1, bytes((_slice))); + } + + /** + * @notice Push a slice to symbol. + */ + function _pushSymbol(string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 1, bytes((_slice))); + } + + /** + * @notice Pop a slice from symbol. + */ + function popSymbol() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 1, 1); + } + + /** + * @notice Pop a slice from symbol. + */ + function _popSymbol() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 1, 1); + } + + /** + * @notice Update a slice of symbol at `_index`. + */ + function updateSymbol(uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 1, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of symbol at `_index`. + */ + function _updateSymbol(uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 1, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Get the full data. + */ + function get() internal view returns (TokenData memory _table) { + bytes32[] memory _keyTuple = new bytes32[](0); + + (bytes memory _staticData, EncodedLengths _encodedLengths, bytes memory _dynamicData) = StoreSwitch.getRecord( + _tableId, + _keyTuple, + _fieldLayout + ); + return decode(_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Get the full data. + */ + function _get() internal view returns (TokenData memory _table) { + bytes32[] memory _keyTuple = new bytes32[](0); + + (bytes memory _staticData, EncodedLengths _encodedLengths, bytes memory _dynamicData) = StoreCore.getRecord( + _tableId, + _keyTuple, + _fieldLayout + ); + return decode(_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using individual values. + */ + function set(uint8 decimals, uint256 totalSupply, address owner, string memory name, string memory symbol) internal { + bytes memory _staticData = encodeStatic(decimals, totalSupply, owner); + + EncodedLengths _encodedLengths = encodeLengths(name, symbol); + bytes memory _dynamicData = encodeDynamic(name, symbol); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using individual values. + */ + function _set(uint8 decimals, uint256 totalSupply, address owner, string memory name, string memory symbol) internal { + bytes memory _staticData = encodeStatic(decimals, totalSupply, owner); + + EncodedLengths _encodedLengths = encodeLengths(name, symbol); + bytes memory _dynamicData = encodeDynamic(name, symbol); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); + } + + /** + * @notice Set the full data using the data struct. + */ + function set(TokenData memory _table) internal { + bytes memory _staticData = encodeStatic(_table.decimals, _table.totalSupply, _table.owner); + + EncodedLengths _encodedLengths = encodeLengths(_table.name, _table.symbol); + bytes memory _dynamicData = encodeDynamic(_table.name, _table.symbol); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using the data struct. + */ + function _set(TokenData memory _table) internal { + bytes memory _staticData = encodeStatic(_table.decimals, _table.totalSupply, _table.owner); + + EncodedLengths _encodedLengths = encodeLengths(_table.name, _table.symbol); + bytes memory _dynamicData = encodeDynamic(_table.name, _table.symbol); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); + } + + /** + * @notice Decode the tightly packed blob of static data using this table's field layout. + */ + function decodeStatic(bytes memory _blob) internal pure returns (uint8 decimals, uint256 totalSupply, address owner) { + decimals = (uint8(Bytes.getBytes1(_blob, 0))); + + totalSupply = (uint256(Bytes.getBytes32(_blob, 1))); + + owner = (address(Bytes.getBytes20(_blob, 33))); + } + + /** + * @notice Decode the tightly packed blob of dynamic data using the encoded lengths. + */ + function decodeDynamic( + EncodedLengths _encodedLengths, + bytes memory _blob + ) internal pure returns (string memory name, string memory symbol) { + uint256 _start; + uint256 _end; + unchecked { + _end = _encodedLengths.atIndex(0); + } + name = (string(SliceLib.getSubslice(_blob, _start, _end).toBytes())); + + _start = _end; + unchecked { + _end += _encodedLengths.atIndex(1); + } + symbol = (string(SliceLib.getSubslice(_blob, _start, _end).toBytes())); + } + + /** + * @notice Decode the tightly packed blobs using this table's field layout. + * @param _staticData Tightly packed static fields. + * @param _encodedLengths Encoded lengths of dynamic fields. + * @param _dynamicData Tightly packed dynamic fields. + */ + function decode( + bytes memory _staticData, + EncodedLengths _encodedLengths, + bytes memory _dynamicData + ) internal pure returns (TokenData memory _table) { + (_table.decimals, _table.totalSupply, _table.owner) = decodeStatic(_staticData); + + (_table.name, _table.symbol) = decodeDynamic(_encodedLengths, _dynamicData); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(uint8 decimals, uint256 totalSupply, address owner) internal pure returns (bytes memory) { + return abi.encodePacked(decimals, totalSupply, owner); + } + + /** + * @notice Tightly pack dynamic data lengths using this table's schema. + * @return _encodedLengths The lengths of the dynamic fields (packed into a single bytes32 value). + */ + function encodeLengths( + string memory name, + string memory symbol + ) internal pure returns (EncodedLengths _encodedLengths) { + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = EncodedLengthsLib.pack(bytes(name).length, bytes(symbol).length); + } + } + + /** + * @notice Tightly pack dynamic (variable length) data using this table's schema. + * @return The dynamic data, encoded into a sequence of bytes. + */ + function encodeDynamic(string memory name, string memory symbol) internal pure returns (bytes memory) { + return abi.encodePacked(bytes((name)), bytes((symbol))); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode( + uint8 decimals, + uint256 totalSupply, + address owner, + string memory name, + string memory symbol + ) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData = encodeStatic(decimals, totalSupply, owner); + + EncodedLengths _encodedLengths = encodeLengths(name, symbol); + bytes memory _dynamicData = encodeDynamic(name, symbol); + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple() internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + return _keyTuple; + } +} diff --git a/packages/world-module-erc20-own-store/test/ERC20.t.sol b/packages/world-module-erc20-own-store/test/ERC20.t.sol new file mode 100644 index 0000000000..5f8481362e --- /dev/null +++ b/packages/world-module-erc20-own-store/test/ERC20.t.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { Test } from "forge-std/Test.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { ERC20 } from "../src/ERC20.sol"; +import { Token } from "../src/codegen/tables/Token.sol"; +import { Balances } from "../src/codegen/tables/Balances.sol"; +import { Allowances } from "../src/codegen/tables/Allowances.sol"; + +contract tokenTest is Test, GasReporter { + ERC20 token; + + address alice = address(0x123); + address charlie = address(0x456); + + function setUp() public { + startGasReport("token constructor"); + token = new ERC20("token", "MUD", address(this), 18); + endGasReport(); + StoreSwitch.setStoreAddress(address(token)); + } + + function testTokenSetUp() public { + assertTrue(address(token) != address(0)); + + assertEq(Token.getDecimals(), 18); + assertEq(Token.getTotalSupply(), 0); + assertEq(Token.getName(), "token"); + assertEq(Token.getSymbol(), "MUD"); + } + + function testTokenMint() public { + startGasReport("token mint"); + token.mint(address(this), 1000); + endGasReport(); + + assertEq(Balances.getBalance(address(this)), 1000); + assertEq(Token.getTotalSupply(), 1000); + } + + function testTokenMintInvalidCaller() public { + vm.expectRevert(); + vm.prank(alice); + startGasReport("token mint invalid caller"); + token.mint(address(alice), 1000); + endGasReport(); + } + + function testTokenMintToZeroAddress() public { + vm.expectRevert(); + startGasReport("token mint to zero address"); + token.mint(address(0), 1000); + endGasReport(); + } + + function testFuzzTokenMint(uint256 mintAmount) public { + startGasReport("fuzz token mint"); + token.mint(address(this), mintAmount); + endGasReport(); + + assertEq(Balances.getBalance(address(this)), mintAmount); + assertEq(Token.getTotalSupply(), mintAmount); + } + + function testTokenBurn() public { + token.mint(address(this), 1000); + startGasReport("token burn"); + token.burn(address(this), 500); + endGasReport(); + + assertEq(Balances.getBalance(address(this)), 500); + assertEq(Token.getTotalSupply(), 500); + } + + function testTokenBurnInvalidCaller() public { + vm.expectRevert(); + vm.prank(alice); + startGasReport("token burn invalid caller"); + token.burn(address(alice), 1000); + endGasReport(); + } + + function testTokenBurnZeroAddress() public { + vm.expectRevert(); + startGasReport("token burn zero address"); + token.burn(address(0), 1000); + endGasReport(); + } + + function testFuzzTokenBurn(uint256 burnAmount) public { + burnAmount = bound(burnAmount, 1, 1000000000); + token.mint(address(this), 1000000000); + + startGasReport("fuzz token burn"); + token.burn(address(this), burnAmount); + endGasReport(); + + assertEq(Balances.getBalance(address(this)), 1000000000 - burnAmount); + assertEq(Token.getTotalSupply(), 1000000000 - burnAmount); + } + + function testTokenTransfer() public { + token.mint(address(this), 1000); + + startGasReport("token transfer"); + token.transfer(alice, 500); + endGasReport(); + + assertEq(Balances.getBalance(address(this)), 500); + assertEq(Balances.getBalance(alice), 500); + } + + function testTokenTransferInsufficientBalance() public { + token.mint(address(this), 1000); + vm.expectRevert(); + + startGasReport("token transfer insufficient balance"); + token.transfer(alice, 1001); + endGasReport(); + } + + function testFuzzTransfer(uint256 transferAmount) public { + transferAmount = bound(transferAmount, 1, 1000000); + token.mint(address(this), 1000000); + + startGasReport("fuzz transfer"); + token.transfer(alice, transferAmount); + endGasReport(); + + assertEq(Balances.getBalance(address(this)), 1000000 - transferAmount); + assertEq(Balances.getBalance(alice), transferAmount); + } + + function testApprove() public { + startGasReport("token approve"); + token.approve(charlie, 1000); + endGasReport(); + + assertEq(Allowances.getApproval(address(this), charlie), 1000); + } + + function testTransferFrom() public { + token.mint(alice, 1000); + + vm.prank(alice); + token.approve(address(this), 500); + + startGasReport("token transferFrom"); + token.transferFrom(alice, charlie, 500); + endGasReport(); + + assertEq(Balances.getBalance(alice), 500); + assertEq(Balances.getBalance(charlie), 500); + assertEq(Allowances.getApproval(address(this), alice), 0); + } + + function testTransferFromInsufficientAllowance() public { + token.mint(address(this), 1000); + vm.expectRevert(); + + startGasReport("token transferFrom insufficient allowance"); + token.transferFrom(charlie, alice, 1000); + endGasReport(); + } + + function testFuzzTransferFrom(uint256 transferAmount) public { + transferAmount = bound(transferAmount, 1, 1000000); + token.mint(alice, 1000000); + + vm.prank(alice); + token.approve(address(this), transferAmount); + + startGasReport("fuzz transferFrom"); + token.transferFrom(alice, charlie, transferAmount); + endGasReport(); + + assertEq(Balances.getBalance(alice), 1000000 - transferAmount); + assertEq(Balances.getBalance(charlie), transferAmount); + assertEq(Allowances.getApproval(address(this), alice), 0); + } + + function testBalanceOf() public { + token.mint(address(this), 1000); + assertEq(token.balanceOf(address(this)), 1000); + } +} diff --git a/packages/world-module-erc20-own-store/ts/build.ts b/packages/world-module-erc20-own-store/ts/build.ts new file mode 100644 index 0000000000..9f24536f54 --- /dev/null +++ b/packages/world-module-erc20-own-store/ts/build.ts @@ -0,0 +1,18 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { tablegen } from "@latticexyz/store/codegen"; +import { worldgen } from "@latticexyz/world/node"; + +/** + * To avoid circular dependencies, we run a very similar `build` step as `cli` package here. + */ + +// TODO: move tablegen/worldgen to CLI commands from store/world we can run in package.json instead of a custom script +// (https://github.com/latticexyz/mud/issues/3030) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const configPath = "../mud.config"; + +const { default: config } = await import(configPath); +const rootDir = path.dirname(path.join(__dirname, configPath)); +await Promise.all([tablegen({ rootDir, config }), worldgen({ rootDir, config })]); diff --git a/packages/world-module-erc20-own-store/tsconfig.json b/packages/world-module-erc20-own-store/tsconfig.json new file mode 100644 index 0000000000..9b0bf57752 --- /dev/null +++ b/packages/world-module-erc20-own-store/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["mud.config.ts", "ts"] +} diff --git a/packages/world-module-erc20-own-store/tsup.config.ts b/packages/world-module-erc20-own-store/tsup.config.ts new file mode 100644 index 0000000000..a89281f3d6 --- /dev/null +++ b/packages/world-module-erc20-own-store/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: { + "mud.config": "mud.config.ts", + }, + target: "esnext", + format: ["esm"], + dts: !process.env.TSUP_SKIP_DTS, + sourcemap: true, + clean: true, + minify: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da5e3efbbf..46d9bd9bcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1290,6 +1290,49 @@ importers: specifier: 0.34.6 version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + packages/world-module-erc20-own-store: + dependencies: + '@latticexyz/schema-type': + specifier: workspace:* + version: link:../schema-type + '@latticexyz/store': + specifier: workspace:* + version: link:../store + '@latticexyz/world': + specifier: workspace:* + version: link:../world + devDependencies: + '@latticexyz/abi-ts': + specifier: workspace:* + version: link:../abi-ts + '@latticexyz/cli': + specifier: workspace:* + version: link:../cli + '@latticexyz/gas-report': + specifier: workspace:* + version: link:../gas-report + '@types/node': + specifier: ^18.15.11 + version: 18.15.11 + ds-test: + specifier: https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0 + version: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0 + forge-std: + specifier: https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/74cfb77e308dd188d2f58864aaf44963ae6b88b1 + solhint: + specifier: ^3.3.7 + version: 3.3.7 + tsup: + specifier: ^6.7.0 + version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) + tsx: + specifier: ^3.12.6 + version: 3.12.6 + vitest: + specifier: 0.34.6 + version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + packages/world-module-metadata: dependencies: '@latticexyz/schema-type':