diff --git a/cli/project-template/.gitignore b/cli/project-template/.gitignore new file mode 100644 index 00000000..aa1e8fc3 --- /dev/null +++ b/cli/project-template/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +my-diamond/ +spec/ +skills/ +*.tsbuildinfo +.vscode/ diff --git a/cli/project-template/README.md b/cli/project-template/README.md new file mode 100644 index 00000000..a3f89f1e --- /dev/null +++ b/cli/project-template/README.md @@ -0,0 +1,92 @@ +# Compose CLI Project Template + +This directory is the current Compose CLI implementation template. It is a Node.js/TypeScript CLI that scaffolds a Diamond-based Foundry project from Compose-owned metadata under `bases/`. + +## Current Behavior + +- `compose` with no command prints help. +- `compose init` starts the interactive init flow. +- Interactive init asks for framework, base, Compose library facets, extension facets, and local example facets. +- Foundry init validates selected facets before writing files. +- Generated Foundry files are written into the current Foundry project when `foundry.toml` exists, otherwise into `my-diamond/`. +- `compose validate` and `compose inspect` are routed placeholders only. + +## Commands + +From this directory: + +```sh +npm install +npm run build +node dist/index.js +node dist/index.js init +``` + +For development: + +```sh +npm run dev -- init +``` + +## Structure + +- `bases/`: Compose-owned metadata for standards, diamond facets, library facets, and local examples. +- `src/index.ts`: CLI entrypoint. +- `src/context`: Shared context types and factory. +- `src/pipelines`: Top-level and command pipelines. +- `src/modules`: Feature logic for entry UI, config loading, validation, scaffolding, routing, and Foundry project detection. +- `src/adapters`: External library adapters, currently hashing through `viem`. +- `src/resolver`: Dependency keys, registry, and resolver. +- `src/utils`: Generic reusable helpers for files, regex, terminal output, and Solidity text parsing. +- `src/templates`: Solidity template assets copied into generated projects. +- `spec/`: M1 design notes and architecture documentation. +- `skills/`: Local Codex architecture skill used while developing this template. + +## Metadata + +`bases/` is the source of truth for interactive init options. + +- `diamond.json`: Diamond facets. +- `libraries.json`: Compose library facets. +- `examples.json`: Local example facets used for validation testing. +- Other JSON files define selectable bases such as ERC-20, ERC-721, ERC-1155, ERC-6909, AccessControl, and Owner. + +Each base has: + +- `required`: Facets always included for that base. +- `optional`: Extension facets shown after the base is selected. + +## Validation + +Foundry init currently validates: + +- Selector exports: every selected facet must export all intended external/public functions. +- Selector collisions: selected facets must not introduce duplicate function selectors. +- Identifier collisions: selected facets must not use incompatible storage layouts for the same storage identifier. + +Validation is fail-fast. `EntryModule.showReport` prints the error report and stops the flow before scaffolding continues. + +## Generated Output + +Generated files are organized by meaning: + +```txt +src/ + diamond/ + libraries/ + facets/ +``` + +The CLI also writes a generated `compose.json` into the target project. `compose.lock` is not part of the current M1 implementation. + +## Local Artifacts + +The local `.gitignore` excludes generated or machine-local files: + +```gitignore +node_modules/ +dist/ +my-diamond/ +*.tsbuildinfo +.vscode/ +``` diff --git a/cli/project-template/bases/accesscontrol.json b/cli/project-template/bases/accesscontrol.json new file mode 100644 index 00000000..d43e5440 --- /dev/null +++ b/cli/project-template/bases/accesscontrol.json @@ -0,0 +1,42 @@ +{ + "access-control": { + "label": "AccessControl", + "required": { + "AccessControlDataFacet": { + "path": "./src/templates/access/AccessControl/Data/AccessControlDataFacet.sol" + }, + "AccessControlGrantFacet": { + "path": "./src/templates/access/AccessControl/Grant/AccessControlGrantFacet.sol" + }, + "AccessControlRevokeFacet": { + "path": "./src/templates/access/AccessControl/Revoke/AccessControlRevokeFacet.sol" + } + }, + "optional": { + "AccessControlAdminFacet": { + "path": "./src/templates/access/AccessControl/Admin/AccessControlAdminFacet.sol" + }, + "AccessControlGrantBatchFacet": { + "path": "./src/templates/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol" + }, + "AccessControlRevokeBatchFacet": { + "path": "./src/templates/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol" + }, + "AccessControlPausableFacet": { + "path": "./src/templates/access/AccessControl/Pausable/AccessControlPausableFacet.sol" + }, + "AccessControlRenounceFacet": { + "path": "./src/templates/access/AccessControl/Renounce/AccessControlRenounceFacet.sol" + }, + "AccessControlTemporalDataFacet": { + "path": "./src/templates/access/AccessControl/Temporal/Data/AccessControlTemporalDataFacet.sol" + }, + "AccessControlTemporalGrantFacet": { + "path": "./src/templates/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol" + }, + "AccessControlTemporalRevokeFacet": { + "path": "./src/templates/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol" + } + } + } +} diff --git a/cli/project-template/bases/diamond.json b/cli/project-template/bases/diamond.json new file mode 100644 index 00000000..3ab7ea8e --- /dev/null +++ b/cli/project-template/bases/diamond.json @@ -0,0 +1,15 @@ +{ + "diamond": { + "label": "Diamond", + "required": { + "DiamondInspectFacet": { + "path": "./src/templates/diamond/DiamondInspectFacet.sol" + } + }, + "optional": { + "DiamondUpgradeFacet": { + "path": "./src/templates/diamond/DiamondUpgradeFacet.sol" + } + } + } +} diff --git a/cli/project-template/bases/erc1155.json b/cli/project-template/bases/erc1155.json new file mode 100644 index 00000000..dc6ed464 --- /dev/null +++ b/cli/project-template/bases/erc1155.json @@ -0,0 +1,27 @@ +{ + "erc-1155": { + "label": "ERC-1155", + "required": { + "ERC1155DataFacet": { + "path": "./src/templates/token/ERC1155/Data/ERC1155DataFacet.sol" + }, + "ERC1155ApproveFacet": { + "path": "./src/templates/token/ERC1155/Approve/ERC1155ApproveFacet.sol" + }, + "ERC1155TransferFacet": { + "path": "./src/templates/token/ERC1155/Transfer/ERC1155TransferFacet.sol" + } + }, + "optional": { + "ERC1155BurnFacet": { + "path": "./src/templates/token/ERC1155/Burn/ERC1155BurnFacet.sol" + }, + "ERC1155MetadataFacet": { + "path": "./src/templates/token/ERC1155/Metadata/ERC1155MetadataFacet.sol" + }, + "RoyaltyFacet": { + "path": "./src/templates/token/Royalty/RoyaltyFacet.sol" + } + } + } +} diff --git a/cli/project-template/bases/erc20.json b/cli/project-template/bases/erc20.json new file mode 100644 index 00000000..33c2dbce --- /dev/null +++ b/cli/project-template/bases/erc20.json @@ -0,0 +1,30 @@ +{ + "erc-20": { + "label": "ERC-20", + "required": { + "ERC20DataFacet": { + "path": "./src/templates/token/ERC20/Data/ERC20DataFacet.sol" + }, + "ERC20ApproveFacet": { + "path": "./src/templates/token/ERC20/Approve/ERC20ApproveFacet.sol" + }, + "ERC20TransferFacet": { + "path": "./src/templates/token/ERC20/Transfer/ERC20TransferFacet.sol" + } + }, + "optional": { + "ERC20BurnFacet": { + "path": "./src/templates/token/ERC20/Burn/ERC20BurnFacet.sol" + }, + "ERC20MetadataFacet": { + "path": "./src/templates/token/ERC20/Metadata/ERC20MetadataFacet.sol" + }, + "ERC20PermitFacet": { + "path": "./src/templates/token/ERC20/Permit/ERC20PermitFacet.sol" + }, + "ERC20BridgeableFacet": { + "path": "./src/templates/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol" + } + } + } +} diff --git a/cli/project-template/bases/erc6969.json b/cli/project-template/bases/erc6969.json new file mode 100644 index 00000000..b21f7121 --- /dev/null +++ b/cli/project-template/bases/erc6969.json @@ -0,0 +1,27 @@ +{ + "erc-6909": { + "label": "ERC-6909", + "required": { + "ERC6909DataFacet": { + "path": "./src/templates/token/ERC6909/Data/ERC6909DataFacet.sol" + }, + "ERC6909TransferFacet": { + "path": "./src/templates/token/ERC6909/Transfer/ERC6909TransferFacet.sol" + }, + "ERC6909ApproveFacet": { + "path": "./src/templates/token/ERC6909/Approve/ERC6909ApproveFacet.sol" + }, + "ERC6909OperatorFacet": { + "path": "./src/templates/token/ERC6909/Operator/ERC6909OperatorFacet.sol" + } + }, + "optional": { + "ERC6909BurnFacet": { + "path": "./src/templates/token/ERC6909/Burn/ERC6909BurnFacet.sol" + }, + "RoyaltyFacet": { + "path": "./src/templates/token/Royalty/RoyaltyFacet.sol" + } + } + } +} diff --git a/cli/project-template/bases/erc721.json b/cli/project-template/bases/erc721.json new file mode 100644 index 00000000..177d3fd9 --- /dev/null +++ b/cli/project-template/bases/erc721.json @@ -0,0 +1,55 @@ +{ + "erc-721": { + "label": "ERC-721", + "required": { + "ERC721ApproveFacet": { + "path": "./src/templates/token/ERC721/Approve/ERC721ApproveFacet.sol" + }, + "ERC721DataFacet": { + "path": "./src/templates/token/ERC721/Data/ERC721DataFacet.sol" + }, + "ERC721TransferFacet": { + "path": "./src/templates/token/ERC721/Transfer/ERC721TransferFacet.sol" + } + }, + "optional": { + "ERC721BurnFacet": { + "path": "./src/templates/token/ERC721/Burn/ERC721BurnFacet.sol" + }, + "ERC721MetadataFacet": { + "path": "./src/templates/token/ERC721/Metadata/ERC721MetadataFacet.sol" + }, + "RoyaltyFacet": { + "path": "./src/templates/token/Royalty/RoyaltyFacet.sol" + } + } + }, + "erc-721-enumerable": { + "label": "ERC-721 Enumerable", + "required": { + "ERC721ApproveFacet": { + "path": "./src/templates/token/ERC721/Approve/ERC721ApproveFacet.sol" + }, + "ERC721DataFacet": { + "path": "./src/templates/token/ERC721/Data/ERC721DataFacet.sol" + }, + "ERC721EnumerableDataFacet": { + "path": "./src/templates/token/ERC721/Enumerable/Data/ERC721EnumerableDataFacet.sol" + }, + "ERC721EnumerableTransferFacet": { + "path": "./src/templates/token/ERC721/Enumerable/Transfer/ERC721EnumerableTransferFacet.sol" + } + }, + "optional": { + "ERC721EnumerableBurnFacet": { + "path": "./src/templates/token/ERC721/Enumerable/Burn/ERC721EnumerableBurnFacet.sol" + }, + "ERC721MetadataFacet": { + "path": "./src/templates/token/ERC721/Metadata/ERC721MetadataFacet.sol" + }, + "RoyaltyFacet": { + "path": "./src/templates/token/Royalty/RoyaltyFacet.sol" + } + } + } +} diff --git a/cli/project-template/bases/examples.json b/cli/project-template/bases/examples.json new file mode 100644 index 00000000..32b56017 --- /dev/null +++ b/cli/project-template/bases/examples.json @@ -0,0 +1,17 @@ +{ + "examples": { + "label": "Local Examples", + "required": {}, + "optional": { + "ERC20MissingExportSelector": { + "path": "./src/templates/examples/ERC20MissingExportSelector.sol" + }, + "ERC20SelectorCollisionError": { + "path": "./src/templates/examples/ERC20SelectorCollisionError.sol" + }, + "ERC20IdentifierCollisionError": { + "path": "./src/templates/examples/ERC20IdentifierCollisionError.sol" + } + } + } +} diff --git a/cli/project-template/bases/libraries.json b/cli/project-template/bases/libraries.json new file mode 100644 index 00000000..1a06a772 --- /dev/null +++ b/cli/project-template/bases/libraries.json @@ -0,0 +1,11 @@ +{ + "libraries": { + "label": "Libraries", + "required": {}, + "optional": { + "ERC165Facet": { + "path": "./src/templates/interfaceDetection/ERC165/ERC165Facet.sol" + } + } + } +} diff --git a/cli/project-template/bases/owner.json b/cli/project-template/bases/owner.json new file mode 100644 index 00000000..1478f5f0 --- /dev/null +++ b/cli/project-template/bases/owner.json @@ -0,0 +1,27 @@ +{ + "owner": { + "label": "Owner", + "required": { + "OwnerDataFacet": { + "path": "./src/templates/access/Owner/Data/OwnerDataFacet.sol" + }, + "OwnerTransferFacet": { + "path": "./src/templates/access/Owner/Transfer/OwnerTransferFacet.sol" + } + }, + "optional": { + "OwnerRenounceFacet": { + "path": "./src/templates/access/Owner/Renounce/OwnerRenounceFacet.sol" + }, + "OwnerTwoStepDataFacet": { + "path": "./src/templates/access/Owner/TwoSteps/Data/OwnerTwoStepDataFacet.sol" + }, + "OwnerTwoStepTransferFacet": { + "path": "./src/templates/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferFacet.sol" + }, + "OwnerTwoStepRenounceFacet": { + "path": "./src/templates/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceFacet.sol" + } + } + } +} diff --git a/cli/project-template/package-lock.json b/cli/project-template/package-lock.json new file mode 100644 index 00000000..0416d731 --- /dev/null +++ b/cli/project-template/package-lock.json @@ -0,0 +1,1758 @@ +{ + "name": "compose-cli-architecture-template", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "compose-cli-architecture-template", + "version": "0.1.0", + "dependencies": { + "@inquirer/prompts": "^8.5.2", + "inquirer": "^8.2.7", + "viem": "^2.52.2" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.1", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", + "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor/node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", + "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.1", + "@inquirer/confirm": "^6.1.1", + "@inquirer/editor": "^5.2.2", + "@inquirer/expand": "^5.1.1", + "@inquirer/input": "^5.1.2", + "@inquirer/number": "^4.1.1", + "@inquirer/password": "^5.1.1", + "@inquirer/rawlist": "^5.3.1", + "@inquirer/search": "^4.2.1", + "@inquirer/select": "^5.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ox": { + "version": "0.14.29", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.29.tgz", + "integrity": "sha512-M5j87Ec4V99MQdRct/g09eWXW60g6zhHTUs1lr4deUtrPDnezBdCJTgKd7pxqTpSZBFveV0ALi9jMMuT1qKyNg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/viem": { + "version": "2.52.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.52.2.tgz", + "integrity": "sha512-HSU12p5aD/kAPZfrlbCUqdiP4P/c6hQ9AhfTS51VbLUQIjkWd1d5EjrCx/SCxZ0zhZVRn4Iv5X5WDqXPG8Ubew==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.29", + "ws": "8.20.1" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/cli/project-template/package.json b/cli/project-template/package.json new file mode 100644 index 00000000..197736fe --- /dev/null +++ b/cli/project-template/package.json @@ -0,0 +1,25 @@ +{ + "name": "compose-cli-architecture-template", + "version": "0.1.0", + "private": true, + "description": "Minimal internal-architecture scaffold for Compose CLI", + "type": "commonjs", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "dev": "tsx src/index.ts" + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.1", + "typescript": "^5.6.3" + }, + "dependencies": { + "@inquirer/prompts": "^8.5.2", + "inquirer": "^8.2.7", + "viem": "^2.52.2" + } +} diff --git a/cli/project-template/src/adapters/hashingAdapter.ts b/cli/project-template/src/adapters/hashingAdapter.ts new file mode 100644 index 00000000..297362ef --- /dev/null +++ b/cli/project-template/src/adapters/hashingAdapter.ts @@ -0,0 +1,12 @@ +import { keccak256, stringToBytes, type Hex } from "viem"; + +export interface HashingAdapterInterface { + keccak256(value: string): Hex; +} + +export const HashingAdapter: HashingAdapterInterface = { + // Hash a UTF-8 string using keccak256 and return the hex digest. + keccak256(value: string): Hex { + return keccak256(stringToBytes(value)); + }, +}; diff --git a/cli/project-template/src/context/context.ts b/cli/project-template/src/context/context.ts new file mode 100644 index 00000000..4062243c --- /dev/null +++ b/cli/project-template/src/context/context.ts @@ -0,0 +1,18 @@ +import { ComposeContext } from "./types"; + +export const Context = { + // Create a fresh command context with empty params, config, state, and successful status. + create(): ComposeContext { + return { + param: {}, + config: {}, + state: {}, + status: { + success: true, + stopped: false, + failedAt: null, + error: null, + }, + }; + }, +}; diff --git a/cli/project-template/src/context/types.ts b/cli/project-template/src/context/types.ts new file mode 100644 index 00000000..eec4b3e4 --- /dev/null +++ b/cli/project-template/src/context/types.ts @@ -0,0 +1,31 @@ +export type ComposeError = { + code: string; + message: string; + nativeError: unknown | null; +}; + +export type ModuleState = { + success: boolean; + result: T | null; + error: ComposeError | null; +}; + +export type ExecutionStatus = { + success: boolean; + stopped: boolean; + failedAt: string | null; + error: ComposeError | null; +}; + +export type ChildPipelineState = { + success: boolean; + state: Record; + status: ExecutionStatus; +}; + +export type ComposeContext = { + param: Record; + config: Record; + state: Record; + status: ExecutionStatus; +}; diff --git a/cli/project-template/src/index.ts b/cli/project-template/src/index.ts new file mode 100644 index 00000000..02fba2a1 --- /dev/null +++ b/cli/project-template/src/index.ts @@ -0,0 +1,20 @@ +import { Context } from "./context/context"; +import { ComposeContext } from "./context/types"; +import { EntryPipeline } from "./pipelines/entryPipeline"; + +// Create the command context, run the entry pipeline, and set the process exit code. +async function main(argv: string[]): Promise { + const ctx: ComposeContext = Context.create(); + ctx.param.argv = argv; + + const result = await EntryPipeline.execute(ctx); + if (!result.status.success) { + process.exitCode = 1; + } +} + +main(process.argv.slice(2)).catch((error: unknown) => { + // Keep failure handling simple in the template. + console.error("Unhandled error:", error); + process.exitCode = 1; +}); diff --git a/cli/project-template/src/modules/configModule.ts b/cli/project-template/src/modules/configModule.ts new file mode 100644 index 00000000..d042ef0e --- /dev/null +++ b/cli/project-template/src/modules/configModule.ts @@ -0,0 +1,102 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { ComposeContext } from "../context/types"; + +export type FacetEntry = { + path: string; +}; + +export type BaseDefinition = { + label: string; + required: Record; + optional: Record; +}; + +type BaseManifest = Record; + +export type BasesCatalog = { + globals: { + diamond?: BaseDefinition; + libraries?: BaseDefinition; + examples?: BaseDefinition; + }; + features: Record; +}; + +// ===================== +// Helper +// ===================== + +const BASE_ORDER = new Map([ + ["erc-20", 20], + ["erc-721", 721], + ["erc-721-enumerable", 722], + ["erc-1155", 1155], + ["erc-6909", 6909], +]); + +// Sort base features by Compose standard order, then by label for custom bases. +function sortBaseFeatures(features: Record): Record { + return Object.fromEntries( + Object.entries(features).sort(([leftKey, leftDefinition], [rightKey, rightDefinition]) => { + const leftOrder = BASE_ORDER.get(leftKey) ?? Number.MAX_SAFE_INTEGER; + const rightOrder = BASE_ORDER.get(rightKey) ?? Number.MAX_SAFE_INTEGER; + + if (leftOrder !== rightOrder) { + return leftOrder - rightOrder; + } + + return leftDefinition.label.localeCompare(rightDefinition.label); + }), + ); +} + +// ===================== +// Modules +// ===================== + +export const ConfigModule = { + // Load Compose-owned base, diamond, library, and example metadata from bases/. + async loadBasesCatalog(ctx: ComposeContext): Promise { + const basesRoot = path.resolve(process.cwd(), "bases"); + const entries = await fs.readdir(basesRoot, { withFileTypes: true }); + + const globals: BasesCatalog["globals"] = {}; + const features: Record = {}; + + for (const entry of entries) { + if (!entry.isFile() || path.extname(entry.name).toLowerCase() !== ".json") { + continue; + } + + const manifestPath = path.join(basesRoot, entry.name); + const raw = await fs.readFile(manifestPath, "utf8"); + const manifest = JSON.parse(raw) as BaseManifest; + + for (const [key, definition] of Object.entries(manifest)) { + if (key === "diamond" || key === "libraries" || key === "examples") { + globals[key] = definition; + continue; + } + + features[key] = definition; + } + } + + const catalog = { globals, features: sortBaseFeatures(features) }; + + ctx.config.bases = catalog; + ctx.state.config = { + success: true, + result: { + baseCount: Object.keys(catalog.features).length, + hasDiamondCatalog: Boolean(globals.diamond), + hasLibrariesCatalog: Boolean(globals.libraries), + hasExamplesCatalog: Boolean(globals.examples), + }, + error: null, + }; + + return ctx; + }, +}; diff --git a/cli/project-template/src/modules/entryModule.ts b/cli/project-template/src/modules/entryModule.ts new file mode 100644 index 00000000..1340e54c --- /dev/null +++ b/cli/project-template/src/modules/entryModule.ts @@ -0,0 +1,434 @@ +import { ComposeContext, ModuleState } from "../context/types"; +import { BasesCatalog } from "./configModule"; +import { cyan, red, yellow } from "../utils/terminal"; + +// ===================== +// Helper +// ===================== + +const COMPOSE_DOCS_URL = "https://compose.diamonds/"; +const VERSION = "0.1.0"; + +const COMPOSE_HEADER = ` + _____ ____ __ __ _____ ____ _____ ______ _____ _ _____ + / ____/ __ \\| \\/ | __ \\ / __ \\ / ____| ____| / ____| | |_ _| + | | | | | | \\ / | |__) | | | | (___ | |__ | | | | | | + | | | | | | |\\/| | ___/| | | |\\___ \\| __| | | | | | | + | |___| |__| | | | | | | |__| |____) | |____ | |____| |____ _| |_ + \\_____\\____/|_| |_|_| \\____/|_____/|______| \\_____|______|_____| + + `; + +const HELP_TEXT = ` +Compose CLI v${VERSION} + +Scaffolds Diamond-based projects using the Compose Library + +Usage: + compose init [options] + compose validate [options] + compose inspect [options] + compose --help + +Options: + --framework + --starter + --out + --help + +For more information about Compose, see: ${COMPOSE_DOCS_URL} +`; + +type PromptApi = { + checkbox: ( + config: { + message: string; + choices: readonly { name: string; value: Value; checked?: boolean }[]; + theme?: { + prefix?: string | { idle?: string; done?: string }; + icon?: { + checked?: string; + unchecked?: string; + cursor?: string; + disabledChecked?: string; + disabledUnchecked?: string; + }; + style?: { + keysHelpTip?: (keys: [key: string, action: string][]) => string | undefined; + }; + }; + }, + ) => Promise; + select: ( + config: { + message: string; + choices: readonly { name: string; value: Value }[]; + default?: Value; + theme?: { + prefix?: string | { idle?: string; done?: string }; + icon?: { + cursor?: string; + }; + style?: { + keysHelpTip?: (keys: [key: string, action: string][]) => string | undefined; + }; + }; + }, + ) => Promise; +}; + +type SelectorExportIssue = { + facetName: string; + path: string; + missingExports: string[]; + extraExports: string[]; +}; + +type SelectorExportValidationResult = { + checkedFacets: number; + issues: SelectorExportIssue[]; +}; + +type SelectorOwner = { + facetName: string; + path: string; + functionName: string; + signature: string; +}; + +type SelectorCollision = { + selector: string; + owners: SelectorOwner[]; +}; + +type SelectorCollisionValidationResult = { + checkedFacets: number; + collisions: SelectorCollision[]; +}; + +type IdentifierCollisionOwner = { + facetName: string; + path: string; + slot: string; + layout: string[]; + source: "erc8042" | "slot-assignment"; + structName: string | null; +}; + +type IdentifierCollision = { + identifier: string; + owners: IdentifierCollisionOwner[]; +}; + +type IdentifierCollisionValidationResult = { + checkedFacets: number; + collisions: IdentifierCollision[]; +}; + +type FacetScanWarning = { + facetName: string; + path: string; + warnings: string[]; +}; + +type FacetScanResult = { + facets: FacetScanWarning[]; +}; + +const checkboxTheme = { + prefix: "", + icon: { + checked: "[✓]", + unchecked: "[ ]", + cursor: ">", + disabledChecked: "[✓]", + disabledUnchecked: "[ ]", + }, + style: { + keysHelpTip: () => undefined, + }, +} as const; + +const selectTheme = { + prefix: "", + icon: { + cursor: ">", + }, + style: { + keysHelpTip: () => undefined, + }, +} as const; + +// Load Inquirer lazily so non-interactive commands do not pay prompt startup cost. +async function loadPrompts(): Promise { + return (await import("@inquirer/prompts")) as unknown as PromptApi; +} + +// Read selector export validation state from context with a typed result. +function getSelectorExportValidationState( + ctx: ComposeContext, +): ModuleState | null { + return (ctx.state.validationSelectorExports as ModuleState | undefined) ?? null; +} + +// Read selector collision validation state from context with a typed result. +function getSelectorCollisionValidationState( + ctx: ComposeContext, +): ModuleState | null { + return ( + ctx.state.validationSelectorCollisions as ModuleState | undefined + ) ?? null; +} + +// Read identifier collision validation state from context with a typed result. +function getIdentifierCollisionValidationState( + ctx: ComposeContext, +): ModuleState | null { + return ( + ctx.state.validationIdentifierCollisions as ModuleState | undefined + ) ?? null; +} + +// Read facet scan state so warnings can be reported before hard failures. +function getFacetScanState(ctx: ComposeContext): ModuleState | null { + return (ctx.state.facetScan as ModuleState | undefined) ?? null; +} + +// ===================== +// Modules +// ===================== + +export const EntryModule = { + // Print the Compose welcome header shown by interactive init. + async showComposeHeader(ctx: ComposeContext): Promise { + console.log(cyan(COMPOSE_HEADER)); + console.log(cyan("Scaffold your diamond smart contracts project with Compose")); + console.log(cyan(`Explore our library: ${COMPOSE_DOCS_URL}\n`)); + + ctx.state.welcome = { + success: true, + result: { + header: COMPOSE_HEADER, + docsUrl: COMPOSE_DOCS_URL, + }, + error: null, + }; + + return ctx; + }, + + // Print top-level command help for empty CLI invocations. + async showHelp(ctx: ComposeContext): Promise { + console.log(HELP_TEXT.trim()); + + ctx.state.entry = { + success: true, + result: { + helpText: HELP_TEXT.trim(), + }, + error: null, + }; + + return ctx; + }, + + // Render validation warnings and fail-fast validation reports. + async showReport(ctx: ComposeContext): Promise { + const facetScan = getFacetScanState(ctx); + const scanWarnings = (facetScan?.result?.facets ?? []) + .map((facet) => ({ + facetName: facet.facetName, + path: facet.path, + warnings: facet.warnings, + })) + .filter((facet) => facet.warnings.length > 0); + + if (scanWarnings.length > 0) { + console.warn(yellow("\nValidation warnings")); + for (const facet of scanWarnings) { + console.warn(`\n${facet.facetName}`); + console.warn(` ${facet.path}`); + for (const warning of facet.warnings) { + console.warn(` ${warning}`); + } + } + } + + const selectorExportValidation = getSelectorExportValidationState(ctx); + + if (selectorExportValidation && !selectorExportValidation.success) { + console.error(red("\nValidation failed")); + console.error(red(selectorExportValidation.error?.message ?? "Validation failed.")); + + for (const issue of selectorExportValidation.result?.issues ?? []) { + console.error(`\n${issue.facetName}`); + console.error(` ${issue.path}`); + + if (issue.missingExports.length > 0) { + console.error(` Missing exports: ${issue.missingExports.join(", ")}`); + } + + if (issue.extraExports.length > 0) { + console.error(` Extra exports: ${issue.extraExports.join(", ")}`); + } + } + + ctx.status = { + success: false, + stopped: true, + failedAt: "validationSelectorExports", + error: selectorExportValidation.error, + }; + + return ctx; + } + + const selectorCollisionValidation = getSelectorCollisionValidationState(ctx); + + if (selectorCollisionValidation && !selectorCollisionValidation.success) { + console.error(red("\nValidation failed")); + console.error(red(selectorCollisionValidation.error?.message ?? "Validation failed.")); + + for (const collision of selectorCollisionValidation.result?.collisions ?? []) { + console.error(`\n${collision.selector}`); + for (const owner of collision.owners) { + console.error(` ${owner.facetName}: ${owner.signature}`); + console.error(` ${owner.path}`); + } + } + + ctx.status = { + success: false, + stopped: true, + failedAt: "validationSelectorCollisions", + error: selectorCollisionValidation.error, + }; + + return ctx; + } + + const identifierCollisionValidation = getIdentifierCollisionValidationState(ctx); + + if (identifierCollisionValidation && !identifierCollisionValidation.success) { + console.error(red("\nValidation failed")); + console.error(red(identifierCollisionValidation.error?.message ?? "Validation failed.")); + + for (const collision of identifierCollisionValidation.result?.collisions ?? []) { + console.error(`\n${collision.identifier}`); + for (const owner of collision.owners) { + console.error(` ${owner.facetName}: [${owner.layout.join(", ")}]`); + console.error(` ${owner.path}`); + } + } + + ctx.status = { + success: false, + stopped: true, + failedAt: "validationIdentifierCollisions", + error: identifierCollisionValidation.error, + }; + } + + return ctx; + }, + + // Run the no-argument interactive init prompt flow. + async runInitInteractive(ctx: ComposeContext): Promise { + const { checkbox, select } = await loadPrompts(); + const catalog = ctx.config.bases as BasesCatalog; + const featureChoices = Object.entries(catalog.features).map(([key, definition]) => ({ + name: definition.label, + value: key, + })); + + const framework = await select({ + message: "Select project framework:", + choices: [ + { name: "Foundry", value: "foundry" }, + { name: "Hardhat", value: "hardhat" }, + ] as const, + default: "foundry", + theme: selectTheme, + }); + + const selectedBaseKey = await select({ + message: "Select base:", + choices: featureChoices, + theme: selectTheme, + }); + + const selectedBase = catalog.features[selectedBaseKey]; + + const mergedRequiredFacets = { + ...(catalog.globals.diamond?.required ?? {}), + ...(catalog.globals.libraries?.required ?? {}), + ...selectedBase.required, + }; + + const availableLibraryFacets = Object.fromEntries( + [ + ...Object.entries(catalog.globals.diamond?.optional ?? {}), + ...Object.entries(catalog.globals.libraries?.optional ?? {}), + ].filter( + ([facetName]) => !(facetName in mergedRequiredFacets), + ), + ); + + const libraryChoices = Object.keys(availableLibraryFacets).map((facetName) => ({ + name: facetName, + value: facetName, + })); + + const selectedLibraries = await checkbox({ + message: "Select Compose library facets:", + choices: libraryChoices, + theme: checkboxTheme, + }); + + const extensionChoices = Object.keys(selectedBase.optional).map((facetName) => ({ + name: facetName, + value: facetName, + })); + + const selectedExtensions = await checkbox({ + message: "Select extension facets:", + choices: extensionChoices, + theme: checkboxTheme, + }); + + const localExampleChoices = Object.keys(catalog.globals.examples?.optional ?? {}).map((facetName) => ({ + name: facetName, + value: facetName, + })); + + const selectedLocalExamples = await checkbox({ + message: "Select local example facets:", + choices: localExampleChoices, + theme: checkboxTheme, + }); + + ctx.param.framework = framework; + ctx.param.base = selectedBaseKey; + ctx.param.extensions = selectedExtensions; + ctx.param.libraries = selectedLibraries; + ctx.param.localExamples = selectedLocalExamples; + + ctx.config.bases = catalog; + ctx.state.entry = { + success: true, + result: { + framework, + selectedBaseKey, + selectedBaseLabel: selectedBase.label, + selectedLibraries, + selectedExtensions, + selectedLocalExamples, + requiredFacets: Object.keys(mergedRequiredFacets), + availableLibraryFacets: Object.keys(availableLibraryFacets), + }, + error: null, + }; + + return ctx; + }, +}; diff --git a/cli/project-template/src/modules/foundryProjectModule.ts b/cli/project-template/src/modules/foundryProjectModule.ts new file mode 100644 index 00000000..7652df32 --- /dev/null +++ b/cli/project-template/src/modules/foundryProjectModule.ts @@ -0,0 +1,33 @@ +import path from "node:path"; +import { ComposeContext } from "../context/types"; +import { pathExists } from "../utils/files"; + +// ===================== +// Modules +// ===================== + +export const FoundryProjectModule = { + // Resolve whether init should write into the current Foundry project or a new folder. + async resolveProjectRoot(ctx: ComposeContext): Promise { + const projectName = String(ctx.param.projectName ?? "my-diamond"); + const outDir = String(ctx.param.outDir ?? process.cwd()); + const currentFoundryToml = path.resolve(process.cwd(), "foundry.toml"); + const projectRoot = (await pathExists(currentFoundryToml)) + ? process.cwd() + : path.resolve(outDir, projectName); + + ctx.param.projectRoot = projectRoot; + ctx.state.foundryProject = { + success: true, + result: { + projectName, + outDir, + projectRoot, + usedCurrentDirectory: projectRoot === process.cwd(), + }, + error: null, + }; + + return ctx; + }, +}; diff --git a/cli/project-template/src/modules/pipelineBuilderModule.ts b/cli/project-template/src/modules/pipelineBuilderModule.ts new file mode 100644 index 00000000..42486380 --- /dev/null +++ b/cli/project-template/src/modules/pipelineBuilderModule.ts @@ -0,0 +1,55 @@ +import { ComposeContext } from "../context/types"; +import { FoundryInitPipeline } from "../pipelines/foundryInitPipeline"; + +// ===================== +// Modules +// ===================== + +export const PipelineBuilderModule = { + // Route the parsed command and framework to the correct command pipeline. + async route(ctx: ComposeContext): Promise { + const command = String(ctx.param.command ?? ""); + const args = (ctx.param.args as string[]) ?? []; + + let framework = String(ctx.param.framework ?? "foundry").toLowerCase(); + for (let i = 0; i < args.length; i++) { + if (args[i] === "--framework" && args[i + 1]) { + framework = args[i + 1].toLowerCase(); + break; + } + } + + switch (ctx.param.command) { + case "init": + ctx.state.commandSelected = { + success: true, + result: { command, framework }, + error: null, + }; + + if (framework === "foundry") { + return FoundryInitPipeline.execute(ctx); + } + ctx.state.commandRouting = { + success: false, + result: null, + error: { + code: "UNSUPPORTED_FRAMEWORK", + message: `Init pipeline is not implemented for framework: ${framework}`, + nativeError: null, + }, + }; + return ctx; + case "validate": + case "inspect": + ctx.state.commandSelected = { + success: true, + result: { command }, + error: null, + }; + return ctx; + default: + return ctx; + } + }, +}; diff --git a/cli/project-template/src/modules/scaffoldingModule.ts b/cli/project-template/src/modules/scaffoldingModule.ts new file mode 100644 index 00000000..24a5ecd1 --- /dev/null +++ b/cli/project-template/src/modules/scaffoldingModule.ts @@ -0,0 +1,516 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { ComposeContext } from "../context/types"; +import { BasesCatalog, FacetEntry } from "./configModule"; +import { + copyFileIfMissing, + resolveLocalSolidityImportClosure, + writeFileIfMissing, +} from "../utils/files"; +import { escapeRegExp } from "../utils/regex"; +import { + parseExportedSelectorNames, + parseSolidityFunctions, + removeSolidityComments, + type SolidityFunctionInfo, +} from "../utils/solidityText"; + +// ===================== +// Helper +// ===================== + +type SeedFile = { + source: string; + target: string; +}; + +type SelectedFacetSource = + | "diamond-required" + | "diamond-optional" + | "library-required" + | "base-required" + | "library-optional" + | "extension" + | "local-example"; + +type SelectedFacet = { + name: string; + source: SelectedFacetSource; + entry: FacetEntry; +}; + +type StorageLayoutInfo = { + slot: string; + layout: string[]; + source: "erc8042" | "slot-assignment"; + structName: string | null; +}; + +type FacetScanResult = { + facetName: string; + source: SelectedFacetSource; + path: string; + contractName: string | null; + functions: SolidityFunctionInfo[]; + exportedSelectors: string[]; + missingExports: string[]; + extraExports: string[]; + storageLayouts: StorageLayoutInfo[]; + warnings: string[]; +}; + +// Extract storage layouts using M1's annotation-first, slot-assignment fallback rule. +function parseStorageLayouts(source: string): { layouts: StorageLayoutInfo[]; warnings: string[] } { + const cleanSource = removeSolidityComments(source); + const layouts: StorageLayoutInfo[] = []; + const warnings: string[] = []; + const structs = parseStructs(cleanSource); + const annotationPattern = /@custom:storage-location\s+erc8042:([A-Za-z0-9_.:-]+)[\s\S]*?\bstruct\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g; + let match: RegExpExecArray | null; + + while ((match = annotationPattern.exec(source)) !== null) { + layouts.push(...buildStorageLayouts(match[1], match[2], structs, "erc8042")); + } + + if (layouts.length > 0) { + return { layouts, warnings }; + } + + const inferredLayouts = parseSlotAssignmentLayouts(cleanSource, structs); + if (inferredLayouts.length > 0) { + warnings.push( + "Missing `@custom:storage-location` annotation -- storage slot inferred from `.slot :=` pattern. Add the annotation for reliable detection.", + ); + return { layouts: inferredLayouts, warnings }; + } + + warnings.push("Cannot determine storage layout -- no ERC-8042 annotation or recognized storage pattern found."); + return { layouts: [], warnings }; +} + +// Collect struct bodies by name so storage layout parsing can resolve fields. +function parseStructs(source: string): Map { + const structs = new Map(); + const structPattern = /\bstruct\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g; + let match: RegExpExecArray | null; + + while ((match = structPattern.exec(source)) !== null) { + const bodyStart = structPattern.lastIndex; + const bodyEnd = findMatchingBrace(source, bodyStart - 1); + if (bodyEnd === -1) { + continue; + } + + structs.set(match[1], source.slice(bodyStart, bodyEnd)); + structPattern.lastIndex = bodyEnd + 1; + } + + return structs; +} + +// Find the closing brace that matches the given opening brace index. +function findMatchingBrace(source: string, openBraceIndex: number): number { + let depth = 0; + for (let i = openBraceIndex; i < source.length; i++) { + if (source[i] === "{") { + depth++; + continue; + } + + if (source[i] === "}") { + depth--; + if (depth === 0) { + return i; + } + } + } + + return -1; +} + +// Build the storage layout record for one namespace and struct pair. +function buildStorageLayouts( + slot: string, + structName: string, + structs: Map, + source: StorageLayoutInfo["source"], +): StorageLayoutInfo[] { + const root = buildStructLayout(structName, structs); + return [ + { + slot, + layout: root, + source, + structName, + }, + ]; +} + +// Infer storage layout records from accessor functions that assign `.slot`. +function parseSlotAssignmentLayouts( + source: string, + structs: Map, +): StorageLayoutInfo[] { + const layouts: StorageLayoutInfo[] = []; + const functionPattern = /\bfunction\b[\s\S]*?\{[\s\S]*?\bassembly\s*\{[\s\S]*?\.slot\s*:=[\s\S]*?\}[\s\S]*?\n\s*\}/g; + let match: RegExpExecArray | null; + + while ((match = functionPattern.exec(source)) !== null) { + const functionSource = match[0]; + const structName = parseReturnedStorageStruct(functionSource); + const slotExpression = parseSlotAssignmentExpression(functionSource); + + if (!structName || !slotExpression) { + continue; + } + + const slot = resolveSlotNamespace(slotExpression, functionSource, source); + if (!slot || !structs.has(structName)) { + continue; + } + + layouts.push(...buildStorageLayouts(slot, structName, structs, "slot-assignment")); + } + + return dedupeStorageLayouts(layouts); +} + +// Read the storage struct returned by an accessor function. +function parseReturnedStorageStruct(functionSource: string): string | null { + const match = functionSource.match(/\breturns\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s+storage\s+[A-Za-z_][A-Za-z0-9_]*\s*\)/); + return match?.[1] ?? null; +} + +// Read the expression assigned to `.slot` inside an assembly block. +function parseSlotAssignmentExpression(functionSource: string): string | null { + const match = functionSource.match(/\.slot\s*:=\s*([A-Za-z_][A-Za-z0-9_]*|"[^"]+"|'[^']+'|0x[0-9a-fA-F]+)\b/); + return match?.[1] ?? null; +} + +// Resolve a `.slot` assignment expression back to its namespace string. +function resolveSlotNamespace( + expression: string, + functionSource: string, + fullSource: string, +): string | null { + const directString = expression.match(/^["']([^"']+)["']$/); + if (directString) { + return directString[1]; + } + + const directConstant = resolveConstantNamespace(expression, fullSource); + if (directConstant) { + return directConstant; + } + + const localAssignmentPattern = new RegExp(`\\bbytes32\\s+${escapeRegExp(expression)}\\s*=\\s*([^;]+);`); + const localAssignment = functionSource.match(localAssignmentPattern); + if (!localAssignment) { + return null; + } + + const localExpression = localAssignment[1].trim(); + const localKeccak = parseKeccakNamespace(localExpression); + if (localKeccak) { + return localKeccak; + } + + return resolveConstantNamespace(localExpression, fullSource); +} + +// Resolve a bytes32 constant declaration into its keccak namespace. +function resolveConstantNamespace(name: string, source: string): string | null { + const constantPattern = new RegExp(`\\bbytes32\\s+(?:internal\\s+|private\\s+|public\\s+)?constant\\s+${escapeRegExp(name)}\\s*=\\s*([^;]+);`); + const match = source.match(constantPattern); + if (!match) { + return null; + } + + return parseKeccakNamespace(match[1]); +} + +// Extract the string namespace from a `keccak256("...")` expression. +function parseKeccakNamespace(expression: string): string | null { + const match = expression.match(/\bkeccak256\s*\(\s*["']([^"']+)["']\s*\)/); + return match?.[1] ?? null; +} + +// Remove duplicate storage layout records produced by repeated source patterns. +function dedupeStorageLayouts(layouts: StorageLayoutInfo[]): StorageLayoutInfo[] { + const seen = new Set(); + return layouts.filter((layout) => { + const key = `${layout.slot}:${layout.structName ?? ""}:${layout.layout.join(",")}`; + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); +} + +// Flatten a struct into the normalized storage type sequence used by validation. +function buildStructLayout( + structName: string, + structs: Map, +): string[] { + const body = structs.get(structName); + if (!body) { + return []; + } + + const layout: string[] = []; + + for (const field of parseStructFields(body)) { + const fieldType = normalizeStorageType(field.type); + + if (structs.has(fieldType)) { + const inlineLayout = buildStructLayout(fieldType, structs); + layout.push(...inlineLayout); + continue; + } + + layout.push(storageTypeToken(fieldType)); + } + + return layout; +} + +// Split a Solidity struct body into field type/name pairs. +function parseStructFields(body: string): { type: string; name: string }[] { + return body + .split(";") + .map((rawField) => rawField.trim()) + .filter(Boolean) + .map((field) => field.replace(/\s+/g, " ")) + .map((field) => { + const match = field.match(/^(.+)\s+([A-Za-z_][A-Za-z0-9_]*)$/); + if (!match) { + return null; + } + + return { type: match[1], name: match[2] }; + }) + .filter((field): field is { type: string; name: string } => field !== null); +} + +// Remove Solidity storage-location keywords from a type string. +function normalizeStorageType(type: string): string { + return type + .replace(/\b(calldata|memory|storage|indexed)\b/g, "") + .replace(/\s+/g, " ") + .trim(); +} + +// Normalize a Solidity storage type for layout comparison. +function storageTypeToken(type: string): string { + let normalized = normalizeStorageType(type) + .replace(/\buint\b/g, "uint256") + .replace(/\bint\b/g, "int256") + .replace(/\s+/g, " ") + .trim(); + + normalized = stripStorageVariableNames(normalized); + normalized = normalized + .replace(/\s*=>\s*/g, "=>") + .replace(/\s*,\s*/g, ",") + .replace(/\s+/g, ""); + + return normalized; +} + +// Remove mapping parameter names so equivalent mapping types compare equally. +function stripStorageVariableNames(type: string): string { + let current = type; + let previous: string; + + do { + previous = current; + current = current.replace( + /\b([A-Za-z_][A-Za-z0-9_]*(?:[0-9]+)?(?:\[[^\]]*\])?)\s+[A-Za-z_][A-Za-z0-9_]*(?=\s*(?:=>|,|\)|$))/g, + "$1", + ); + } while (current !== previous); + + return current; +} + +// Resolve all facets selected by the current init context. +function getSelectedFacets(ctx: ComposeContext): SelectedFacet[] { + const catalog = ctx.config.bases as BasesCatalog; + const selectedBaseKey = String(ctx.param.base ?? ""); + const selectedBase = catalog.features[selectedBaseKey]; + const selectedLibraryNames = new Set((ctx.param.libraries as string[] | undefined) ?? []); + const selectedExtensionNames = new Set((ctx.param.extensions as string[] | undefined) ?? []); + const selectedLocalExampleNames = new Set((ctx.param.localExamples as string[] | undefined) ?? []); + + const selected: SelectedFacet[] = []; + + for (const [name, entry] of Object.entries(catalog.globals.diamond?.required ?? {})) { + selected.push({ name, entry, source: "diamond-required" }); + } + + for (const [name, entry] of Object.entries(catalog.globals.libraries?.required ?? {})) { + selected.push({ name, entry, source: "library-required" }); + } + + for (const [name, entry] of Object.entries(selectedBase.required)) { + selected.push({ name, entry, source: "base-required" }); + } + + for (const [name, entry] of Object.entries(catalog.globals.diamond?.optional ?? {})) { + if (selectedLibraryNames.has(name)) { + selected.push({ name, entry, source: "diamond-optional" }); + } + } + + for (const [name, entry] of Object.entries(catalog.globals.libraries?.optional ?? {})) { + if (selectedLibraryNames.has(name)) { + selected.push({ name, entry, source: "library-optional" }); + } + } + + for (const [name, entry] of Object.entries(selectedBase.optional)) { + if (selectedExtensionNames.has(name)) { + selected.push({ name, entry, source: "extension" }); + } + } + + for (const [name, entry] of Object.entries(catalog.globals.examples?.optional ?? {})) { + if (selectedLocalExampleNames.has(name)) { + selected.push({ name, entry, source: "local-example" }); + } + } + + return selected; +} + +// Choose the scaffold target directory based on the selected facet category. +function targetDirectoryForFacet(root: string, facet: SelectedFacet): string { + if (facet.source === "diamond-required" || facet.source === "diamond-optional") { + return path.join(root, "src", "diamond"); + } + + if (facet.source === "library-required" || facet.source === "library-optional") { + return path.join(root, "src", "libraries"); + } + + return path.join(root, "src", "facets"); +} + +// ===================== +// Modules +// ===================== + +export const ScaffoldingModule = { + // Scan selected facets for functions, exported selectors, and storage layouts. + async scanSelectedFacets(ctx: ComposeContext): Promise { + const selectedFacets = getSelectedFacets(ctx); + const results: FacetScanResult[] = []; + + for (const facet of selectedFacets) { + const resolvedPath = path.resolve(process.cwd(), facet.entry.path); + const source = await fs.readFile(resolvedPath, "utf8"); + const contractMatch = source.match(/\bcontract\s+([A-Za-z_][A-Za-z0-9_]*)\b/); + const functions = parseSolidityFunctions(source); + const exportedSelectors = parseExportedSelectorNames(source); + const exportedSet = new Set(exportedSelectors); + const functionNames = new Set(functions.map((fn) => fn.name)); + const warnings: string[] = []; + const storageScan = parseStorageLayouts(source); + + if (exportedSelectors.length === 0) { + warnings.push("exportSelectors() was not found or did not export any this..selector entries."); + } + warnings.push(...storageScan.warnings); + + results.push({ + facetName: facet.name, + source: facet.source, + path: facet.entry.path, + contractName: contractMatch?.[1] ?? null, + functions, + exportedSelectors, + missingExports: functions.filter((fn) => !exportedSet.has(fn.name)).map((fn) => fn.signature), + extraExports: exportedSelectors.filter((name) => !functionNames.has(name)), + storageLayouts: storageScan.layouts, + warnings, + }); + } + + ctx.state.facetScan = { + success: results.every((result) => result.missingExports.length === 0 && result.extraExports.length === 0), + result: { + facets: results, + facetCount: results.length, + selectorHashing: "not-computed", + onchain: false, + }, + error: null, + }; + + return ctx; + }, + + // Create the minimal Foundry project layout and copy selected facet sources. + async scaffoldFoundryLayout(ctx: ComposeContext): Promise { + const root = String(ctx.param.projectRoot ?? ""); + const selectedFacets = getSelectedFacets(ctx); + + await fs.mkdir(root, { recursive: true }); + await fs.mkdir(path.join(root, "src"), { recursive: true }); + await fs.mkdir(path.join(root, "src", "diamond"), { recursive: true }); + await fs.mkdir(path.join(root, "src", "libraries"), { recursive: true }); + await fs.mkdir(path.join(root, "src", "facets"), { recursive: true }); + await fs.mkdir(path.join(root, "script"), { recursive: true }); + await fs.mkdir(path.join(root, "test"), { recursive: true }); + + const allSeeds: SeedFile[] = selectedFacets.map((facet) => ({ + source: path.resolve(process.cwd(), facet.entry.path), + target: path.join(targetDirectoryForFacet(root, facet), path.basename(facet.entry.path)), + })); + const seedSources = allSeeds.map((x) => x.source); + const closure = await resolveLocalSolidityImportClosure(seedSources); + + const sourceToTarget = new Map(); + for (const seed of allSeeds) { + sourceToTarget.set(path.resolve(seed.source), seed.target); + } + + for (const file of closure) { + const directTarget = sourceToTarget.get(path.resolve(file)); + if (directTarget) { + await copyFileIfMissing(file, directTarget); + continue; + } + + // For transitive imports, co-locate with the importer directory in target by filename. + // This keeps relative import structure stable for local imports. + const parentSeed = allSeeds.find((seed) => file.startsWith(path.dirname(path.resolve(seed.source)))); + const baseTargetDir = parentSeed ? path.dirname(parentSeed.target) : path.join(root, "src"); + await copyFileIfMissing(file, path.join(baseTargetDir, path.basename(file))); + } + + const foundryToml = `[profile.default]\nsrc = "src"\nout = "out"\nlibs = ["lib"]\n`; + const remappings = "@perfect-abstractions/compose/=lib/compose/src/\n"; + const gitignore = "out/\ncache/\n"; + const readme = `# ${String(ctx.param.projectName)}\n\nGenerated by compose init (demo)\n`; + + await writeFileIfMissing(path.join(root, "foundry.toml"), foundryToml); + await writeFileIfMissing(path.join(root, "remappings.txt"), remappings); + await writeFileIfMissing(path.join(root, ".gitignore"), gitignore); + await writeFileIfMissing(path.join(root, "README.md"), readme); + + return ctx; + }, + + // Write the generated compose.json into the resolved project root. + async writeComposeConfig(ctx: ComposeContext): Promise { + const root = String(ctx.param.projectRoot ?? ""); + const composeJson = ctx.config.composeJson; + + await fs.writeFile(path.join(root, "compose.json"), `${JSON.stringify(composeJson, null, 2)}\n`, "utf8"); + + return ctx; + }, +}; diff --git a/cli/project-template/src/modules/validationModule.ts b/cli/project-template/src/modules/validationModule.ts new file mode 100644 index 00000000..57d5a4c8 --- /dev/null +++ b/cli/project-template/src/modules/validationModule.ts @@ -0,0 +1,297 @@ +import { ComposeContext, ModuleState } from "../context/types"; +import { HashingAdapterInterface } from "../adapters/hashingAdapter"; + +// ===================== +// Helper +// ===================== + +type FunctionInfo = { + name: string; + signature: string; + visibility: "external" | "public"; +}; + +type FacetScanResult = { + facetName: string; + path: string; + functions: FunctionInfo[]; + exportedSelectors: string[]; + missingExports: string[]; + extraExports: string[]; + storageLayouts: StorageLayoutInfo[]; +}; + +type FacetScanStateResult = { + facets: FacetScanResult[]; + facetCount: number; +}; + +type SelectorExportIssue = { + facetName: string; + path: string; + missingExports: string[]; + extraExports: string[]; +}; + +type SelectorOwner = { + facetName: string; + path: string; + functionName: string; + signature: string; +}; + +type SelectorCollision = { + selector: string; + owners: SelectorOwner[]; +}; + +type SelectorCollisionDeps = { + hashing: HashingAdapterInterface; +}; + +type StorageLayoutInfo = { + slot: string; + layout: string[]; + source: "erc8042" | "slot-assignment"; + structName: string | null; +}; + +type IdentifierCollisionOwner = { + facetName: string; + path: string; + slot: string; + layout: string[]; + source: StorageLayoutInfo["source"]; + structName: string | null; +}; + +type IdentifierCollision = { + identifier: string; + owners: IdentifierCollisionOwner[]; +}; + +// Read the facet scan result that all validation stages depend on. +function getFacetScanResult(ctx: ComposeContext): FacetScanStateResult | null { + const state = ctx.state.facetScan as ModuleState | undefined; + return state?.result ?? null; +} + +// Find facets whose exportSelectors list is incomplete or references missing functions. +function findSelectorExportIssues(facets: FacetScanResult[]): SelectorExportIssue[] { + return facets + .map((facet) => ({ + facetName: facet.facetName, + path: facet.path, + missingExports: facet.missingExports, + extraExports: facet.extraExports, + })) + .filter((issue) => issue.missingExports.length > 0 || issue.extraExports.length > 0); +} + +// Compute exported function selectors and group duplicate selector owners. +function findSelectorCollisions( + facets: FacetScanResult[], + hashing: HashingAdapterInterface, +): SelectorCollision[] { + const ownersBySelector = new Map(); + + for (const facet of facets) { + const functionsByName = new Map(facet.functions.map((fn) => [fn.name, fn])); + + for (const exportedName of facet.exportedSelectors) { + const fn = functionsByName.get(exportedName); + if (!fn) { + continue; + } + + const selector = hashing.keccak256(fn.signature).slice(0, 10); + const owners = ownersBySelector.get(selector) ?? []; + owners.push({ + facetName: facet.facetName, + path: facet.path, + functionName: fn.name, + signature: fn.signature, + }); + ownersBySelector.set(selector, owners); + } + } + + return [...ownersBySelector.entries()] + .map(([selector, owners]) => ({ selector, owners })) + .filter((collision) => new Set(collision.owners.map((owner) => owner.facetName)).size > 1); +} + +// Group storage layouts by identifier and keep only incompatible groups. +function findIdentifierCollisions(facets: FacetScanResult[]): IdentifierCollision[] { + const ownersByIdentifier = new Map(); + + for (const facet of facets) { + for (const storageLayout of facet.storageLayouts) { + const identifier = buildIdentifier(storageLayout.slot); + const owners = ownersByIdentifier.get(identifier) ?? []; + owners.push({ + facetName: facet.facetName, + path: facet.path, + slot: storageLayout.slot, + layout: storageLayout.layout, + source: storageLayout.source, + structName: storageLayout.structName, + }); + ownersByIdentifier.set(identifier, owners); + } + } + + return [...ownersByIdentifier.entries()] + .filter(([, owners]) => owners.length > 1) + .filter(([, owners]) => !areLayoutsPrefixCompatible(owners.map((owner) => owner.layout))) + .map(([identifier, owners]) => ({ identifier, owners })); +} + +// Build the identifier key used to group storage layouts. +function buildIdentifier(slot: string): string { + return slot; +} + +// Check whether all layouts are safe prefix extensions of the shortest layout. +function areLayoutsPrefixCompatible(layouts: string[][]): boolean { + if (layouts.length <= 1) { + return true; + } + + const sorted = [...layouts].sort((a, b) => a.length - b.length); + const shortest = sorted[0]; + + return sorted.every((layout) => isPrefix(shortest, layout)); +} + +// Return true when the first layout is a prefix of the second layout. +function isPrefix(prefix: string[], layout: string[]): boolean { + if (prefix.length > layout.length) { + return false; + } + + return prefix.every((value, index) => layout[index] === value); +} + +// ===================== +// Modules +// ===================== + +export const ValidationModule = { + // Validate that every public/external function is exported by exportSelectors. + async validateSelectorExports(ctx: ComposeContext): Promise { + const facetScan = getFacetScanResult(ctx); + + if (!facetScan) { + ctx.state.validationSelectorExports = { + success: false, + result: null, + error: { + code: "FACET_SCAN_MISSING", + message: "Facet scan must run before selector export validation.", + nativeError: null, + }, + }; + return ctx; + } + + const issues = findSelectorExportIssues(facetScan.facets); + const success = issues.length === 0; + + ctx.state.validationSelectorExports = { + success, + result: { + checkedFacets: facetScan.facetCount, + issues, + }, + error: success + ? null + : { + code: "SELECTOR_EXPORT_INVALID", + message: "One or more selected facets do not export all intended selectors.", + nativeError: null, + }, + }; + + return ctx; + }, + + // Detect duplicate 4-byte selectors across selected exported facet functions. + async detectSelectorCollisions( + ctx: ComposeContext, + { hashing }: SelectorCollisionDeps, + ): Promise { + const facetScan = getFacetScanResult(ctx); + + if (!facetScan) { + ctx.state.validationSelectorCollisions = { + success: false, + result: null, + error: { + code: "FACET_SCAN_MISSING", + message: "Facet scan must run before selector collision detection.", + nativeError: null, + }, + }; + return ctx; + } + + const collisions = findSelectorCollisions(facetScan.facets, hashing); + const success = collisions.length === 0; + + ctx.state.validationSelectorCollisions = { + success, + result: { + checkedFacets: facetScan.facetCount, + collisions, + }, + error: success + ? null + : { + code: "SELECTOR_COLLISION_DETECTED", + message: "Two or more selected facets export the same function selector.", + nativeError: null, + }, + }; + + return ctx; + }, + + // Detect incompatible storage layouts sharing the same storage identifier. + async detectIdentifierCollisions(ctx: ComposeContext): Promise { + const facetScan = getFacetScanResult(ctx); + + if (!facetScan) { + ctx.state.validationIdentifierCollisions = { + success: false, + result: null, + error: { + code: "FACET_SCAN_MISSING", + message: "Facet scan must run before identifier collision detection.", + nativeError: null, + }, + }; + return ctx; + } + + const collisions = findIdentifierCollisions(facetScan.facets); + const success = collisions.length === 0; + + ctx.state.validationIdentifierCollisions = { + success, + result: { + checkedFacets: facetScan.facetCount, + collisions, + }, + error: success + ? null + : { + code: "IDENTIFIER_COLLISION_DETECTED", + message: "Two or more selected facets use incompatible storage layouts for the same identifier.", + nativeError: null, + }, + }; + + return ctx; + }, +}; diff --git a/cli/project-template/src/pipelines/entryPipeline.ts b/cli/project-template/src/pipelines/entryPipeline.ts new file mode 100644 index 00000000..1c5fe4c6 --- /dev/null +++ b/cli/project-template/src/pipelines/entryPipeline.ts @@ -0,0 +1,48 @@ +import { ComposeContext } from "../context/types"; +import { ConfigModule } from "../modules/configModule"; +import { EntryModule } from "../modules/entryModule"; +import { PipelineBuilderModule } from "../modules/pipelineBuilderModule"; + +export const EntryPipeline = { + // Execute top-level CLI flow: parse command, run interactive entry work, route, and report. + async execute(ctx: ComposeContext): Promise { + + const argv = (ctx.param.argv as string[]) ?? []; + + if (argv.length === 0) { + ctx = await EntryModule.showHelp(ctx); + return ctx; + } + + ctx.param.command = argv[0] ?? ""; + ctx.param.args = argv.slice(1); + const args = (ctx.param.args as string[]) ?? []; + + if (ctx.param.command === "init" && args.length === 0) { + ctx = await EntryModule.showComposeHeader(ctx); + ctx = await ConfigModule.loadBasesCatalog(ctx); + ctx = await EntryModule.runInitInteractive(ctx); + } + + ctx = await PipelineBuilderModule.route(ctx); + ctx = await EntryModule.showReport(ctx); + + if (ctx.status.stopped) { + return ctx; + } + + if (!ctx.state.commandSelected && !ctx.state.commandRouting) { + ctx.state.commandRouting = { + success: false, + result: null, + error: { + code: "COMMAND_NOT_SUPPORTED", + message: "Unknown or missing command.", + nativeError: null, + }, + }; + } + + return ctx; + }, +}; diff --git a/cli/project-template/src/pipelines/foundryInitPipeline.ts b/cli/project-template/src/pipelines/foundryInitPipeline.ts new file mode 100644 index 00000000..eec48a68 --- /dev/null +++ b/cli/project-template/src/pipelines/foundryInitPipeline.ts @@ -0,0 +1,58 @@ +import { ComposeContext, ModuleState } from "../context/types"; +import { ValidationModule } from "../modules/validationModule"; +import { ScaffoldingModule } from "../modules/scaffoldingModule"; +import { FoundryProjectModule } from "../modules/foundryProjectModule"; +import { DependencyKey } from "../resolver/dependencyKey"; +import { DependencyResolver } from "../resolver/dependencyResolver"; + +export const FoundryInitPipeline = { + // Execute Foundry init by validating selected facets, resolving the project root, and scaffolding. + async execute(ctx: ComposeContext): Promise { + ctx = await ScaffoldingModule.scanSelectedFacets(ctx); + ctx = await ValidationModule.validateSelectorExports(ctx); + + const selectorExportValidation = ctx.state.validationSelectorExports as ModuleState | undefined; + if (selectorExportValidation && !selectorExportValidation.success) { + return ctx; + } + + const deps = await DependencyResolver.resolve([ + { key: DependencyKey.Hashing }, + ]); + + if (!deps.hashing) { + throw new Error("Hashing dependency was not resolved."); + } + + ctx = await ValidationModule.detectSelectorCollisions(ctx, { + hashing: deps.hashing, + }); + + const selectorCollisionValidation = ctx.state.validationSelectorCollisions as ModuleState | undefined; + if (selectorCollisionValidation && !selectorCollisionValidation.success) { + return ctx; + } + + ctx = await ValidationModule.detectIdentifierCollisions(ctx); + + const identifierCollisionValidation = ctx.state.validationIdentifierCollisions as ModuleState | undefined; + if (identifierCollisionValidation && !identifierCollisionValidation.success) { + return ctx; + } + + ctx = await FoundryProjectModule.resolveProjectRoot(ctx); + ctx = await ScaffoldingModule.scaffoldFoundryLayout(ctx); + ctx = await ScaffoldingModule.writeComposeConfig(ctx); + + ctx.state.initPipeline = { + success: true, + result: { + message: "Foundry demo project generated.", + projectRoot: ctx.param.projectRoot, + }, + error: null, + }; + + return ctx; + }, +}; diff --git a/cli/project-template/src/resolver/dependencyKey.ts b/cli/project-template/src/resolver/dependencyKey.ts new file mode 100644 index 00000000..8e4882ca --- /dev/null +++ b/cli/project-template/src/resolver/dependencyKey.ts @@ -0,0 +1,3 @@ +export enum DependencyKey { + Hashing = "hashing", +} diff --git a/cli/project-template/src/resolver/dependencyRegistry.ts b/cli/project-template/src/resolver/dependencyRegistry.ts new file mode 100644 index 00000000..8403fc9e --- /dev/null +++ b/cli/project-template/src/resolver/dependencyRegistry.ts @@ -0,0 +1,22 @@ +import { + HashingAdapter, + HashingAdapterInterface, +} from "../adapters/hashingAdapter"; +import { DependencyKey } from "./dependencyKey"; + +export type DependencyParams = Record; + +export type DependencyFactory = ( + params?: DependencyParams, +) => Promise | T; + +export type DependencyMap = { + [DependencyKey.Hashing]: HashingAdapterInterface; +}; + +// Map dependency keys to adapter factories used by the resolver. +export const DependencyRegistry: { + [Key in DependencyKey]: DependencyFactory; +} = { + [DependencyKey.Hashing]: () => HashingAdapter, +}; diff --git a/cli/project-template/src/resolver/dependencyResolver.ts b/cli/project-template/src/resolver/dependencyResolver.ts new file mode 100644 index 00000000..88a37bcc --- /dev/null +++ b/cli/project-template/src/resolver/dependencyResolver.ts @@ -0,0 +1,32 @@ +import { + DependencyMap, + DependencyParams, + DependencyRegistry, +} from "./dependencyRegistry"; +import { DependencyKey } from "./dependencyKey"; + +export type DependencyRequest = { + key: DependencyKey; + params?: DependencyParams; +}; + +export const DependencyResolver = { + // Resolve only the adapter dependencies explicitly requested by the pipeline. + async resolve( + requests: DependencyRequest[], + ): Promise> { + const deps: Partial = {}; + + for (const request of requests) { + const factory = DependencyRegistry[request.key]; + + if (!factory) { + throw new Error(`Dependency factory not found: ${request.key}`); + } + + deps[request.key] = await factory(request.params); + } + + return deps; + }, +}; diff --git a/cli/project-template/src/templates/access/AccessControl/Admin/AccessControlAdminFacet.sol b/cli/project-template/src/templates/access/AccessControl/Admin/AccessControlAdminFacet.sol new file mode 100644 index 00000000..51f590fc --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Admin/AccessControlAdminFacet.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlAdminFacet { + /** + * @notice Emitted when the admin role for a role is changed. + * @param _role The role that was changed. + * @param _previousAdminRole The previous admin role. + * @param _newAdminRole The new admin role. + */ + event RoleAdminChanged(bytes32 indexed _role, bytes32 indexed _previousAdminRole, bytes32 indexed _newAdminRole); + + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Storage slot identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Returns the storage for the AccessControl. + * @return s The storage for the AccessControl. + */ + function getStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Sets the admin role for a role. + * @param _role The role to set the admin for. + * @param _adminRole The new admin role to set. + * @dev Emits a {RoleAdminChanged} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the current admin of the role. + */ + function setRoleAdmin(bytes32 _role, bytes32 _adminRole) external { + AccessControlStorage storage s = getStorage(); + bytes32 previousAdminRole = s.adminRole[_role]; + + /** + * Check if the caller is the current admin of the role. + */ + if (!s.hasRole[msg.sender][previousAdminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, previousAdminRole); + } + + s.adminRole[_role] = _adminRole; + emit RoleAdminChanged(_role, previousAdminRole, _adminRole); + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.setRoleAdmin.selector); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Admin/AccessControlAdminMod.sol b/cli/project-template/src/templates/access/AccessControl/Admin/AccessControlAdminMod.sol new file mode 100644 index 00000000..57c7794d --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Admin/AccessControlAdminMod.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Emitted when the admin role for a role is changed. + * @param _role The role that was changed. + * @param _previousAdminRole The previous admin role. + * @param _newAdminRole The new admin role. + */ +event RoleAdminChanged(bytes32 indexed _role, bytes32 indexed _previousAdminRole, bytes32 indexed _newAdminRole); + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/* + * @notice Storage slot identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice Storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/** + * @notice Returns the storage for the AccessControl. + * @return s The storage for the AccessControl. + */ +function getStorage() pure returns (AccessControlStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Sets the admin role for a role. + * @param _role The role to set the admin for. + * @param _adminRole The new admin role to set. + * @dev Emits a {RoleAdminChanged} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the current admin of the role. + */ +function setRoleAdmin(bytes32 _role, bytes32 _adminRole) { + AccessControlStorage storage s = getStorage(); + bytes32 previousAdminRole = s.adminRole[_role]; + + /** + * Check if the caller is the current admin of the role. + */ + if (!s.hasRole[msg.sender][previousAdminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, previousAdminRole); + } + + s.adminRole[_role] = _adminRole; + emit RoleAdminChanged(_role, previousAdminRole, _adminRole); +} diff --git a/cli/project-template/src/templates/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol b/cli/project-template/src/templates/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol new file mode 100644 index 00000000..6183c03c --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlGrantBatchFacet { + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Emitted when a role is granted to an account. + * @param _role The role that was granted. + * @param _account The account that was granted the role. + * @param _sender The sender that granted the role. + */ + event RoleGranted(bytes32 indexed _role, address indexed _account, address indexed _sender); + + /** + * @notice Storage slot identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Returns the storage for the AccessControl. + * @return s The storage for the AccessControl. + */ + function getStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Grants a role to multiple accounts in a single transaction. + * @param _role The role to grant. + * @param _accounts The accounts to grant the role to. + * @dev Emits a {RoleGranted} event for each newly granted account. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ + function grantRoleBatch(bytes32 _role, address[] calldata _accounts) external { + AccessControlStorage storage s = getStorage(); + bytes32 adminRole = s.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!s.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + uint256 length = _accounts.length; + for (uint256 i = 0; i < length; i++) { + address account = _accounts[i]; + bool _hasRole = s.hasRole[account][_role]; + if (!_hasRole) { + s.hasRole[account][_role] = true; + emit RoleGranted(_role, account, msg.sender); + } + } + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.grantRoleBatch.selector); + } +} + diff --git a/cli/project-template/src/templates/access/AccessControl/Batch/Grant/AccessControlGrantBatchMod.sol b/cli/project-template/src/templates/access/AccessControl/Batch/Grant/AccessControlGrantBatchMod.sol new file mode 100644 index 00000000..23d2f14f --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Batch/Grant/AccessControlGrantBatchMod.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/** + * @notice Emitted when a role is granted to an account. + * @param _role The role that was granted. + * @param _account The account that was granted the role. + * @param _sender The sender that granted the role. + */ +event RoleGranted(bytes32 indexed _role, address indexed _account, address indexed _sender); + +/* + * @notice Storage slot identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/** + * @notice Returns the storage for the AccessControl. + * @return _s The storage for the AccessControl. + */ +function getStorage() pure returns (AccessControlStorage storage _s) { + bytes32 position = STORAGE_POSITION; + assembly { + _s.slot := position + } +} + +/** + * @notice function to grant a role to multiple accounts in a single transaction. + * @param _role The role to grant. + * @param _accounts The accounts to grant the role to. + * @dev Emits a {RoleGranted} event for each newly granted account. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ +function grantRoleBatch(bytes32 _role, address[] calldata _accounts) { + AccessControlStorage storage s = getStorage(); + bytes32 adminRole = s.adminRole[_role]; + + if (!s.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + uint256 length = _accounts.length; + for (uint256 i = 0; i < length; i++) { + address account = _accounts[i]; + bool _hasRole = s.hasRole[account][_role]; + if (!_hasRole) { + s.hasRole[account][_role] = true; + emit RoleGranted(_role, account, msg.sender); + } + } +} + diff --git a/cli/project-template/src/templates/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol b/cli/project-template/src/templates/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol new file mode 100644 index 00000000..2f0cbfa1 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlRevokeBatchFacet { + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Emitted when a role is revoked from an account. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ + event RoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + + /** + * @notice Storage slot identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Returns the storage for the AccessControl. + * @return s The storage for the AccessControl. + */ + function getStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Revokes a role from multiple accounts in a single transaction. + * @param _role The role to revoke. + * @param _accounts The accounts to revoke the role from. + * @dev Emits a {RoleRevoked} event for each account the role is revoked from. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ + function revokeRoleBatch(bytes32 _role, address[] calldata _accounts) external { + AccessControlStorage storage s = getStorage(); + bytes32 adminRole = s.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!s.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + uint256 length = _accounts.length; + for (uint256 i = 0; i < length; i++) { + address account = _accounts[i]; + bool _hasRole = s.hasRole[account][_role]; + if (_hasRole) { + s.hasRole[account][_role] = false; + emit RoleRevoked(_role, account, msg.sender); + } + } + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.revokeRoleBatch.selector); + } +} + diff --git a/cli/project-template/src/templates/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchMod.sol b/cli/project-template/src/templates/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchMod.sol new file mode 100644 index 00000000..2439ad06 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchMod.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/** + * @notice Emitted when a role is revoked from an account. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ +event RoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + +/* + * @notice Storage slot identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/** + * @notice Returns the storage for the AccessControl. + * @return _s The storage for the AccessControl. + */ +function getStorage() pure returns (AccessControlStorage storage _s) { + bytes32 position = STORAGE_POSITION; + assembly { + _s.slot := position + } +} + +/** + * @notice function to revoke a role from multiple accounts in a single transaction. + * @param _role The role to revoke. + * @param _accounts The accounts to revoke the role from. + * @dev Emits a {RoleRevoked} event for each account the role is revoked from. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ +function revokeRoleBatch(bytes32 _role, address[] calldata _accounts) { + AccessControlStorage storage s = getStorage(); + bytes32 adminRole = s.adminRole[_role]; + + if (!s.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + uint256 length = _accounts.length; + for (uint256 i = 0; i < length; i++) { + address account = _accounts[i]; + bool _hasRole = s.hasRole[account][_role]; + if (_hasRole) { + s.hasRole[account][_role] = false; + emit RoleRevoked(_role, account, msg.sender); + } + } +} + diff --git a/cli/project-template/src/templates/access/AccessControl/Data/AccessControlDataFacet.sol b/cli/project-template/src/templates/access/AccessControl/Data/AccessControlDataFacet.sol new file mode 100644 index 00000000..9ecb108e --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Data/AccessControlDataFacet.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlDataFacet { + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Storage slot identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Returns the storage for the AccessControl. + * @return s The storage for the AccessControl. + */ + function getStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns if an account has a role. + * @param _role The role to check. + * @param _account The account to check the role for. + * @return True if the account has the role, false otherwise. + */ + function hasRole(bytes32 _role, address _account) external view returns (bool) { + AccessControlStorage storage s = getStorage(); + return s.hasRole[_account][_role]; + } + + /** + * @notice Checks if an account has a required role. + * @param _role The role to check. + * @param _account The account to check the role for. + * @custom:error AccessControlUnauthorizedAccount If the account does not have the role. + */ + function requireRole(bytes32 _role, address _account) external view { + AccessControlStorage storage s = getStorage(); + if (!s.hasRole[_account][_role]) { + revert AccessControlUnauthorizedAccount(_account, _role); + } + } + + /** + * @notice Returns the admin role for a role. + * @param _role The role to get the admin for. + * @return The admin role for the role. + */ + function getRoleAdmin(bytes32 _role) external view returns (bytes32) { + AccessControlStorage storage s = getStorage(); + return s.adminRole[_role]; + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.hasRole.selector, this.requireRole.selector, this.getRoleAdmin.selector); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Data/AccessControlDataMod.sol b/cli/project-template/src/templates/access/AccessControl/Data/AccessControlDataMod.sol new file mode 100644 index 00000000..1c315e9d --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Data/AccessControlDataMod.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/* + * @notice Storage slot identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/* + * @notice Default admin role. + */ +bytes32 constant DEFAULT_ADMIN_ROLE = 0x00; + +/** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/** + * @notice Returns the storage for the AccessControl. + * @return _s The storage for the AccessControl. + */ +function getStorage() pure returns (AccessControlStorage storage _s) { + bytes32 position = STORAGE_POSITION; + assembly { + _s.slot := position + } +} + +/** + * @notice function to check if an account has a required role. + * @param _role The role to assert. + * @param _account The account to assert the role for. + * @custom:error AccessControlUnauthorizedAccount If the account does not have the role. + */ +function requireRole(bytes32 _role, address _account) view { + AccessControlStorage storage s = getStorage(); + if (!s.hasRole[_account][_role]) { + revert AccessControlUnauthorizedAccount(_account, _role); + } +} + +/** + * @notice function to check if an account has a role. + * @param _role The role to check. + * @param _account The account to check the role for. + * @return True if the account has the role, false otherwise. + */ +function hasRole(bytes32 _role, address _account) view returns (bool) { + AccessControlStorage storage s = getStorage(); + return s.hasRole[_account][_role]; +} diff --git a/cli/project-template/src/templates/access/AccessControl/Grant/AccessControlGrantFacet.sol b/cli/project-template/src/templates/access/AccessControl/Grant/AccessControlGrantFacet.sol new file mode 100644 index 00000000..0b0991af --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Grant/AccessControlGrantFacet.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlGrantFacet { + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Emitted when a role is granted to an account. + * @param _role The role that was granted. + * @param _account The account that was granted the role. + * @param _sender The sender that granted the role. + */ + event RoleGranted(bytes32 indexed _role, address indexed _account, address indexed _sender); + + /** + * @notice Storage slot identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Returns the storage for the AccessControl. + * @return s The storage for the AccessControl. + */ + function getStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Grants a role to an account. + * @param _role The role to grant. + * @param _account The account to grant the role to. + * @dev Emits a {RoleGranted} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ + function grantRole(bytes32 _role, address _account) external { + AccessControlStorage storage s = getStorage(); + bytes32 adminRole = s.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!s.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + bool _hasRole = s.hasRole[_account][_role]; + if (!_hasRole) { + s.hasRole[_account][_role] = true; + emit RoleGranted(_role, _account, msg.sender); + } + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.grantRole.selector); + } +} + diff --git a/cli/project-template/src/templates/access/AccessControl/Grant/AccessControlGrantMod.sol b/cli/project-template/src/templates/access/AccessControl/Grant/AccessControlGrantMod.sol new file mode 100644 index 00000000..3493bc76 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Grant/AccessControlGrantMod.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/** + * @notice Emitted when a role is granted to an account. + * @param _role The role that was granted. + * @param _account The account that was granted the role. + * @param _sender The sender that granted the role. + */ +event RoleGranted(bytes32 indexed _role, address indexed _account, address indexed _sender); + +/* + * @notice Storage slot identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/** + * @notice Returns the storage for the AccessControl. + * @return _s The storage for the AccessControl. + */ +function getStorage() pure returns (AccessControlStorage storage _s) { + bytes32 position = STORAGE_POSITION; + assembly { + _s.slot := position + } +} + +/** + * @notice function to grant a role to an account. + * @param _role The role to grant. + * @param _account The account to grant the role to. + * @return True if the role was granted, false otherwise. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ +function grantRole(bytes32 _role, address _account) returns (bool) { + AccessControlStorage storage s = getStorage(); + bytes32 adminRole = s.adminRole[_role]; + + if (!s.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + bool _hasRole = s.hasRole[_account][_role]; + if (!_hasRole) { + s.hasRole[_account][_role] = true; + emit RoleGranted(_role, _account, msg.sender); + return true; + } + return false; +} + diff --git a/cli/project-template/src/templates/access/AccessControl/Pausable/AccessControlPausableFacet.sol b/cli/project-template/src/templates/access/AccessControl/Pausable/AccessControlPausableFacet.sol new file mode 100644 index 00000000..956b21f2 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Pausable/AccessControlPausableFacet.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlPausableFacet { + /** + * @notice Event emitted when a role is paused. + * @param _role The role that was paused. + * @param _account The account that paused the role. + */ + event RolePaused(bytes32 indexed _role, address indexed _account); + + /** + * @notice Event emitted when a role is unpaused. + * @param _role The role that was unpaused. + * @param _account The account that unpaused the role. + */ + event RoleUnpaused(bytes32 indexed _role, address indexed _account); + + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + + /** + * @notice Storage slot identifier for AccessControl (reused to access roles). + */ + bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for AccessControl (reused struct definition). + * @dev Must match the struct definition in AccessControlDataFacet / AccessControlDataMod. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + + /** + * @notice Returns the storage for AccessControl. + * @return s The AccessControl storage struct. + */ + function getAccessControlStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns if a role is paused. + * @param _role The role to check. + * @return True if the role is paused, false otherwise. + */ + function isRolePaused(bytes32 _role) external view returns (bool) { + return getStorage().pausedRoles[_role]; + } + + /** + * @notice Temporarily disables a role, preventing all accounts from using it. + * @param _role The role to pause. + * @dev Only the admin of the role can pause it. + * Emits a {RolePaused} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ + function pauseRole(bytes32 _role) external { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlPausableStorage storage s = getStorage(); + + /** + * Get the admin role for this role + */ + bytes32 adminRole = acs.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!acs.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + /** + * Pause the role + */ + s.pausedRoles[_role] = true; + emit RolePaused(_role, msg.sender); + } + + /** + * @notice Re-enables a role that was previously paused. + * @param _role The role to unpause. + * @dev Only the admin of the role can unpause it. + * Emits a {RoleUnpaused} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ + function unpauseRole(bytes32 _role) external { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlPausableStorage storage s = getStorage(); + + /** + * Get the admin role for this role + */ + bytes32 adminRole = acs.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!acs.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + /** + * Unpause the role + */ + s.pausedRoles[_role] = false; + emit RoleUnpaused(_role, msg.sender); + } + + /** + * @notice Checks if an account has a role and if the role is not paused. + * @param _role The role to check. + * @param _account The account to check the role for. + * @custom:error AccessControlUnauthorizedAccount If the account does not have the role. + * @custom:error AccessControlRolePaused If the role is paused. + */ + function requireRoleNotPaused(bytes32 _role, address _account) external view { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlPausableStorage storage s = getStorage(); + + /** + * First check if the account has the role + */ + if (!acs.hasRole[_account][_role]) { + revert AccessControlUnauthorizedAccount(_account, _role); + } + + /** + * Then check if the role is paused + */ + if (s.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat( + this.isRolePaused.selector, + this.pauseRole.selector, + this.unpauseRole.selector, + this.requireRoleNotPaused.selector + ); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Pausable/AccessControlPausableMod.sol b/cli/project-template/src/templates/access/AccessControl/Pausable/AccessControlPausableMod.sol new file mode 100644 index 00000000..53575e14 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Pausable/AccessControlPausableMod.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Event emitted when a role is paused. + * @param _role The role that was paused. + * @param _account The account that paused the role. + */ +event RolePaused(bytes32 indexed _role, address indexed _account); + +/** + * @notice Event emitted when a role is unpaused. + * @param _role The role that was unpaused. + * @param _account The account that unpaused the role. + */ +event RoleUnpaused(bytes32 indexed _role, address indexed _account); + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + +/* + * @notice Storage slot identifier for AccessControl (reused to access roles). + */ +bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice Storage struct for AccessControl (reused struct definition). + * @dev Must match the struct definition in AccessControlDataFacet / AccessControlDataMod. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + +/** + * @notice Returns the storage for AccessControl. + * @return s The AccessControl storage struct. + */ +function getAccessControlStorage() pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice function to check if a role is paused. + * @param _role The role to check. + * @return True if the role is paused, false otherwise. + */ +function isRolePaused(bytes32 _role) view returns (bool) { + AccessControlPausableStorage storage s = getStorage(); + return s.pausedRoles[_role]; +} + +/** + * @notice function to pause a role. + * @param _role The role to pause. + */ +function pauseRole(bytes32 _role) { + AccessControlPausableStorage storage s = getStorage(); + s.pausedRoles[_role] = true; + emit RolePaused(_role, msg.sender); +} + +/** + * @notice function to unpause a role. + * @param _role The role to unpause. + */ +function unpauseRole(bytes32 _role) { + AccessControlPausableStorage storage s = getStorage(); + s.pausedRoles[_role] = false; + emit RoleUnpaused(_role, msg.sender); +} + +/** + * @notice function to check if an account has a role and if the role is not paused. + * @param _role The role to check. + * @param _account The account to check the role for. + * @custom:error AccessControlUnauthorizedAccount If the account does not have the role. + * @custom:error AccessControlRolePaused If the role is paused. + */ +function requireRoleNotPaused(bytes32 _role, address _account) view { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlPausableStorage storage s = getStorage(); + + /** + * First check if the account has the role + */ + if (!acs.hasRole[_account][_role]) { + revert AccessControlUnauthorizedAccount(_account, _role); + } + + /** + * Then check if the role is paused + */ + if (s.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Renounce/AccessControlRenounceFacet.sol b/cli/project-template/src/templates/access/AccessControl/Renounce/AccessControlRenounceFacet.sol new file mode 100644 index 00000000..4f0e196b --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Renounce/AccessControlRenounceFacet.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlRenounceFacet { + /** + * @notice Emitted when a role is revoked from an account. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ + event RoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + + /** + * @notice Thrown when the sender is not the account to renounce the role from. + * @param _sender The sender that is not the account to renounce the role from. + * @param _account The account to renounce the role from. + */ + error AccessControlUnauthorizedSender(address _sender, address _account); + + /** + * @notice Storage slot identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Returns the storage for the AccessControl. + * @return s The storage for the AccessControl. + */ + function getStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Renounces a role from the caller. + * @param _role The role to renounce. + * @param _account The account to renounce the role from. + * @dev Emits a {RoleRevoked} event. + * @custom:error AccessControlUnauthorizedSender If the caller is not the account to renounce the role from. + */ + function renounceRole(bytes32 _role, address _account) external { + AccessControlStorage storage s = getStorage(); + + /** + * Check If the caller is not the account to renounce the role from. + */ + if (msg.sender != _account) { + revert AccessControlUnauthorizedSender(msg.sender, _account); + } + bool _hasRole = s.hasRole[_account][_role]; + if (_hasRole) { + s.hasRole[_account][_role] = false; + emit RoleRevoked(_role, _account, msg.sender); + } + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.renounceRole.selector); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Renounce/AccessControlRenounceMod.sol b/cli/project-template/src/templates/access/AccessControl/Renounce/AccessControlRenounceMod.sol new file mode 100644 index 00000000..204abfdf --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Renounce/AccessControlRenounceMod.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Emitted when a role is revoked from an account. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ +event RoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + +/** + * @notice Thrown when the sender is not the account to renounce the role from. + * @param _sender The sender that is not the account to renounce the role from. + * @param _account The account to renounce the role from. + */ +error AccessControlUnauthorizedSender(address _sender, address _account); + +/* + * @notice Storage slot identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/** + * @notice Returns the storage for the AccessControl. + * @return _s The storage for the AccessControl. + */ +function getStorage() pure returns (AccessControlStorage storage _s) { + bytes32 position = STORAGE_POSITION; + assembly { + _s.slot := position + } +} + +/** + * @notice function to renounce a role from the caller. + * @param _role The role to renounce. + * @param _account The account to renounce the role from. + * @dev Emits a {RoleRevoked} event. + * @custom:error AccessControlUnauthorizedSender If the caller is not the account to renounce the role from. + */ +function renounceRole(bytes32 _role, address _account) { + AccessControlStorage storage s = getStorage(); + + if (msg.sender != _account) { + revert AccessControlUnauthorizedSender(msg.sender, _account); + } + bool _hasRole = s.hasRole[_account][_role]; + if (_hasRole) { + s.hasRole[_account][_role] = false; + emit RoleRevoked(_role, _account, msg.sender); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Revoke/AccessControlRevokeFacet.sol b/cli/project-template/src/templates/access/AccessControl/Revoke/AccessControlRevokeFacet.sol new file mode 100644 index 00000000..7b5bbf43 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Revoke/AccessControlRevokeFacet.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlRevokeFacet { + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Emitted when a role is revoked from an account. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ + event RoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + + /** + * @notice Storage slot identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Returns the storage for the AccessControl. + * @return s The storage for the AccessControl. + */ + function getStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Revokes a role from an account. + * @param _role The role to revoke. + * @param _account The account to revoke the role from. + * @dev Emits a {RoleRevoked} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ + function revokeRole(bytes32 _role, address _account) external { + AccessControlStorage storage s = getStorage(); + bytes32 adminRole = s.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!s.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + bool _hasRole = s.hasRole[_account][_role]; + if (_hasRole) { + s.hasRole[_account][_role] = false; + emit RoleRevoked(_role, _account, msg.sender); + } + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.revokeRole.selector); + } +} + diff --git a/cli/project-template/src/templates/access/AccessControl/Revoke/AccessControlRevokeMod.sol b/cli/project-template/src/templates/access/AccessControl/Revoke/AccessControlRevokeMod.sol new file mode 100644 index 00000000..842db628 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Revoke/AccessControlRevokeMod.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/** + * @notice Emitted when a role is revoked from an account. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ +event RoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + +/* + * @notice Storage slot identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/** + * @notice Returns the storage for the AccessControl. + * @return _s The storage for the AccessControl. + */ +function getStorage() pure returns (AccessControlStorage storage _s) { + bytes32 position = STORAGE_POSITION; + assembly { + _s.slot := position + } +} + +/** + * @notice function to revoke a role from an account. + * @param _role The role to revoke. + * @param _account The account to revoke the role from. + * @return True if the role was revoked, false otherwise. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ +function revokeRole(bytes32 _role, address _account) returns (bool) { + AccessControlStorage storage s = getStorage(); + bytes32 adminRole = s.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!s.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + bool _hasRole = s.hasRole[_account][_role]; + if (_hasRole) { + s.hasRole[_account][_role] = false; + emit RoleRevoked(_role, _account, msg.sender); + return true; + } + return false; +} + diff --git a/cli/project-template/src/templates/access/AccessControl/Temporal/Data/AccessControlTemporalDataFacet.sol b/cli/project-template/src/templates/access/AccessControl/Temporal/Data/AccessControlTemporalDataFacet.sol new file mode 100644 index 00000000..ff5e8692 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Temporal/Data/AccessControlTemporalDataFacet.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlTemporalDataFacet { + /** + * @notice Event emitted when a role is granted with an expiry timestamp. + * @param _role The role that was granted. + * @param _account The account that was granted the role. + * @param _expiresAt The timestamp when the role expires. + * @param _sender The account that granted the role. + */ + event RoleGrantedWithExpiry( + bytes32 indexed _role, address indexed _account, uint256 _expiresAt, address indexed _sender + ); + + /** + * @notice Event emitted when a temporal role is revoked. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ + event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Storage slot identifier for AccessControl (reused to access roles). + */ + bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for AccessControl (reused struct definition). + * @dev Must match the struct definition in AccessControlDataFacet. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Returns the storage for AccessControl. + * @return s The AccessControl storage struct. + */ + function getAccessControlStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the expiry timestamp for a role assignment. + * @param _role The role to check. + * @param _account The account to check. + * @return The expiry timestamp, or 0 if no expiry is set. + */ + function getRoleExpiry(bytes32 _role, address _account) external view returns (uint256) { + return getStorage().roleExpiry[_account][_role]; + } + + /** + * @notice Checks if a role assignment has expired. + * @param _role The role to check. + * @param _account The account to check. + * @return True if the role has expired or doesn't exist, false if still valid. + */ + function isRoleExpired(bytes32 _role, address _account) external view returns (bool) { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlTemporalStorage storage s = getStorage(); + uint256 expiry = s.roleExpiry[_account][_role]; + + /** + * If no expiry set (0), role is valid if account has it + */ + if (expiry == 0) { + return !acs.hasRole[_account][_role]; + } + + /** + * Role is expired if current time is past expiry + */ + return block.timestamp >= expiry; + } + + /** + * @notice Checks if an account has a valid (non-expired) role. + * @param _role The role to check. + * @param _account The account to check the role for. + * @custom:error AccessControlUnauthorizedAccount If the account does not have the role. + * @custom:error AccessControlRoleExpired If the role has expired. + */ + function requireValidRole(bytes32 _role, address _account) external view { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlTemporalStorage storage s = getStorage(); + + /** + * Check if account has the role + */ + if (!acs.hasRole[_account][_role]) { + revert AccessControlUnauthorizedAccount(_account, _role); + } + + /** + * Check if role has expired + */ + uint256 expiry = s.roleExpiry[_account][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, _account); + } + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.getRoleExpiry.selector, this.isRoleExpired.selector, this.requireValidRole.selector); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Temporal/Data/AccessControlTemporalDataMod.sol b/cli/project-template/src/templates/access/AccessControl/Temporal/Data/AccessControlTemporalDataMod.sol new file mode 100644 index 00000000..23c1216e --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Temporal/Data/AccessControlTemporalDataMod.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Event emitted when a role is granted with an expiry timestamp. + * @param _role The role that was granted. + * @param _account The account that was granted the role. + * @param _expiresAt The timestamp when the role expires. + * @param _sender The account that granted the role. + */ +event RoleGrantedWithExpiry( + bytes32 indexed _role, address indexed _account, uint256 _expiresAt, address indexed _sender +); + +/** + * @notice Event emitted when a temporal role is revoked. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ +event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/* + * @notice Storage slot identifier for AccessControl. + */ +bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice Storage struct for AccessControl (reused struct definition). + * @dev Must match the struct definition in AccessControlDataFacet. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Returns the storage for AccessControl. + * @return s The AccessControl storage struct. + */ +function getAccessControlStorage() pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the expiry timestamp for a role assignment. + * @param _role The role to check. + * @param _account The account to check. + * @return The expiry timestamp, or 0 if no expiry is set. + */ +function getRoleExpiry(bytes32 _role, address _account) view returns (uint256) { + return getStorage().roleExpiry[_account][_role]; +} + +/** + * @notice Checks if a role assignment has expired. + * @param _role The role to check. + * @param _account The account to check. + * @return True if the role has expired or doesn't exist, false if still valid. + */ +function isRoleExpired(bytes32 _role, address _account) view returns (bool) { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlTemporalStorage storage s = getStorage(); + uint256 expiry = s.roleExpiry[_account][_role]; + + /** + * If no expiry set (0), role is valid if account has it + */ + if (expiry == 0) { + return !acs.hasRole[_account][_role]; + } + + /** + * Role is expired if current time is past expiry + */ + return block.timestamp >= expiry; +} + +/** + * @notice Checks if an account has a valid (non-expired) role. + * @param _role The role to check. + * @param _account The account to check the role for. + * @custom:error AccessControlUnauthorizedAccount If the account does not have the role. + * @custom:error AccessControlRoleExpired If the role has expired. + */ +function requireValidRole(bytes32 _role, address _account) view { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlTemporalStorage storage s = getStorage(); + + /** + * Check if account has the role + */ + if (!acs.hasRole[_account][_role]) { + revert AccessControlUnauthorizedAccount(_account, _role); + } + + /** + * Check if role has expired + */ + uint256 expiry = s.roleExpiry[_account][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, _account); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol b/cli/project-template/src/templates/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol new file mode 100644 index 00000000..c8615746 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlTemporalGrantFacet { + /** + * @notice Event emitted when a role is granted with an expiry timestamp. + * @param _role The role that was granted. + * @param _account The account that was granted the role. + * @param _expiresAt The timestamp when the role expires. + * @param _sender The account that granted the role. + */ + event RoleGrantedWithExpiry( + bytes32 indexed _role, address indexed _account, uint256 _expiresAt, address indexed _sender + ); + + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Storage slot identifier for AccessControl (reused to access roles). + */ + bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for AccessControl (reused struct definition). + * @dev Must match the struct definition in AccessControlDataFacet. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Returns the storage for AccessControl. + * @return s The AccessControl storage struct. + */ + function getAccessControlStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Grants a role to an account with an expiry timestamp. + * @param _role The role to grant. + * @param _account The account to grant the role to. + * @param _expiresAt The timestamp when the role should expire (must be in the future). + * @dev Only the admin of the role can grant it with expiry. + * Emits a {RoleGrantedWithExpiry} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ + function grantRoleWithExpiry(bytes32 _role, address _account, uint256 _expiresAt) external { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlTemporalStorage storage s = getStorage(); + bytes32 adminRole = acs.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!acs.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + /** + * Require expiry is in the future + */ + if (_expiresAt <= block.timestamp) { + revert AccessControlRoleExpired(_role, _account); + } + + /** + * Grant the role + */ + bool _hasRole = acs.hasRole[_account][_role]; + if (!_hasRole) { + acs.hasRole[_account][_role] = true; + } + + /** + * Set expiry timestamp + */ + s.roleExpiry[_account][_role] = _expiresAt; + emit RoleGrantedWithExpiry(_role, _account, _expiresAt, msg.sender); + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.grantRoleWithExpiry.selector); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantMod.sol b/cli/project-template/src/templates/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantMod.sol new file mode 100644 index 00000000..71dca03d --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantMod.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Event emitted when a role is granted with an expiry timestamp. + * @param _role The role that was granted. + * @param _account The account that was granted the role. + * @param _expiresAt The timestamp when the role expires. + * @param _sender The account that granted the role. + */ +event RoleGrantedWithExpiry( + bytes32 indexed _role, address indexed _account, uint256 _expiresAt, address indexed _sender +); + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/* + * @notice Storage slot identifier for AccessControl (reused to access roles). + */ +bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice Storage struct for AccessControl (reused struct definition). + * @dev Must match the struct definition in AccessControlDataFacet. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Returns the storage for AccessControl. + * @return s The AccessControl storage struct. + */ +function getAccessControlStorage() pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Grants a role to an account with an expiry timestamp. + * @param _role The role to grant. + * @param _account The account to grant the role to. + * @param _expiresAt The timestamp when the role should expire (must be in the future). + * @dev Only the admin of the role can grant it with expiry. + * Emits a {RoleGrantedWithExpiry} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ +function grantRoleWithExpiry(bytes32 _role, address _account, uint256 _expiresAt) { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlTemporalStorage storage s = getStorage(); + bytes32 adminRole = acs.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!acs.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + /** + * Require expiry is in the future + */ + if (_expiresAt <= block.timestamp) { + revert AccessControlRoleExpired(_role, _account); + } + + /** + * Grant the role + */ + bool _hasRole = acs.hasRole[_account][_role]; + if (!_hasRole) { + acs.hasRole[_account][_role] = true; + } + + /** + * Set expiry timestamp + */ + s.roleExpiry[_account][_role] = _expiresAt; + emit RoleGrantedWithExpiry(_role, _account, _expiresAt, msg.sender); +} diff --git a/cli/project-template/src/templates/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol b/cli/project-template/src/templates/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol new file mode 100644 index 00000000..e8395980 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract AccessControlTemporalRevokeFacet { + /** + * @notice Event emitted when a temporal role is revoked. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ + event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + /** + * @notice Storage slot identifier for AccessControl (reused to access roles). + */ + bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice Storage struct for AccessControl (reused struct definition). + * @dev Must match the struct definition in AccessControlDataFacet. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; + } + + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Returns the storage for AccessControl. + * @return s The AccessControl storage struct. + */ + function getAccessControlStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Revokes a temporal role from an account. + * @param _role The role to revoke. + * @param _account The account to revoke the role from. + * @dev Only the admin of the role can revoke it. + * Emits a {TemporalRoleRevoked} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ + function revokeTemporalRole(bytes32 _role, address _account) external { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlTemporalStorage storage s = getStorage(); + bytes32 adminRole = acs.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!acs.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + /** + * Revoke the role + */ + bool _hasRole = acs.hasRole[_account][_role]; + + /** + * Only revoke if the role is currently granted + */ + if (_hasRole) { + /** + * Revoke the role from AccessControl storage + */ + acs.hasRole[_account][_role] = false; + + /** + * Clear expiry timestamp + */ + s.roleExpiry[_account][_role] = 0; + + emit TemporalRoleRevoked(_role, _account, msg.sender); + } + } + + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.revokeTemporalRole.selector); + } +} diff --git a/cli/project-template/src/templates/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeMod.sol b/cli/project-template/src/templates/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeMod.sol new file mode 100644 index 00000000..0ee20098 --- /dev/null +++ b/cli/project-template/src/templates/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeMod.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Event emitted when a temporal role is revoked. + * @param _role The role that was revoked. + * @param _account The account from which the role was revoked. + * @param _sender The account that revoked the role. + */ +event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +/* + * @notice Storage slot identifier for AccessControl (reused to access roles). + */ +bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice Storage struct for AccessControl (reused struct definition). + * @dev Must match the struct definition in AccessControlDataFacet. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +} + +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Returns the storage for AccessControl. + * @return s The AccessControl storage struct. + */ +function getAccessControlStorage() pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Revokes a temporal role from an account. + * @param _role The role to revoke. + * @param _account The account to revoke the role from. + * @dev Only the admin of the role can revoke it. + * Emits a {TemporalRoleRevoked} event. + * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. + */ +function revokeTemporalRole(bytes32 _role, address _account) { + AccessControlStorage storage acs = getAccessControlStorage(); + AccessControlTemporalStorage storage s = getStorage(); + bytes32 adminRole = acs.adminRole[_role]; + + /** + * Check if the caller is the admin of the role. + */ + if (!acs.hasRole[msg.sender][adminRole]) { + revert AccessControlUnauthorizedAccount(msg.sender, adminRole); + } + + /** + * Revoke the role + */ + bool _hasRole = acs.hasRole[_account][_role]; + + /** + * Only revoke if the role is currently granted + */ + if (_hasRole) { + /** + * Revoke the role from AccessControl storage + */ + acs.hasRole[_account][_role] = false; + + /** + * Clear expiry timestamp + */ + s.roleExpiry[_account][_role] = 0; + + emit TemporalRoleRevoked(_role, _account, msg.sender); + } +} diff --git a/cli/project-template/src/templates/access/Owner/Data/OwnerDataFacet.sol b/cli/project-template/src/templates/access/Owner/Data/OwnerDataFacet.sol new file mode 100644 index 00000000..fa87b366 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/Data/OwnerDataFacet.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title OwnerDataFacet + */ +contract OwnerDataFacet { + /** + * @notice Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc173.owner"); + + /** + * @custom:storage-location erc8042:erc173.owner + */ + struct OwnerStorage { + address owner; + } + + /** + * @notice Returns the owner storage struct. + * @return s The owner storage struct. + */ + function getStorage() internal pure returns (OwnerStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Get the address of the owner + * @return The address of the owner. + */ + function owner() external view returns (address) { + return getStorage().owner; + } + + /** + * @notice Exports the function selectors of the OwnerDataFacet + * @dev Used as a selector discovery mechanism for diamonds. + * @return selectors The exported function selectors of the OwnerDataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.owner.selector); + } +} diff --git a/cli/project-template/src/templates/access/Owner/Data/OwnerDataMod.sol b/cli/project-template/src/templates/access/Owner/Data/OwnerDataMod.sol new file mode 100644 index 00000000..12d7b395 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/Data/OwnerDataMod.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC-173 Contract Ownership + * @notice Provides internal functions and storage layout for owner management. + */ + +/** + * @dev This emits when ownership of a contract changes. + */ +event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + +/* + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ +error OwnerUnauthorizedAccount(); +/* + * @notice Thrown when attempting to transfer ownership from a renounced state. + */ +error OwnerAlreadyRenounced(); + +bytes32 constant STORAGE_POSITION = keccak256("erc173.owner"); + +/** + * @custom:storage-location erc8042:erc173.owner + */ +struct OwnerStorage { + address owner; +} + +/** + * @notice Returns a pointer to the ERC-173 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ +function getStorage() pure returns (OwnerStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Sets the stored owner and emits `OwnershipTransferred` with `previousOwner == address(0)`. + * @dev Does not enforce access control. Use from trusted init paths (for example the diamond constructor); + * for guarded changes, use a transfer facet with `OwnerTransferMod` or similar. + * @param _initialOwner Address written to `OwnerStorage.owner`. + */ +function setContractOwner(address _initialOwner) { + OwnerStorage storage s = getStorage(); + s.owner = _initialOwner; + emit OwnershipTransferred(address(0), _initialOwner); +} + +/** + * @notice Get the address of the owner + * @return The address of the owner. + */ +function owner() view returns (address) { + return getStorage().owner; +} + +/** + * @notice Reverts if the caller is not the owner. + */ +function requireOwner() view { + if (getStorage().owner != msg.sender) { + revert OwnerUnauthorizedAccount(); + } +} diff --git a/cli/project-template/src/templates/access/Owner/Renounce/OwnerRenounceFacet.sol b/cli/project-template/src/templates/access/Owner/Renounce/OwnerRenounceFacet.sol new file mode 100644 index 00000000..6f9efcf6 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/Renounce/OwnerRenounceFacet.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-173 Contract Facet to Renounce Ownership + */ +contract OwnerRenounceFacet { + /** + * @dev This emits when ownership of a contract changes. + */ + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ + error OwnerUnauthorizedAccount(); + + bytes32 constant STORAGE_POSITION = keccak256("erc173.owner"); + + /** + * @custom:storage-location erc8042:erc173.owner + */ + struct OwnerStorage { + address owner; + } + + /** + * @notice Returns a pointer to the owner storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ + function getStorage() internal pure returns (OwnerStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Renounce ownership of the contract + * @dev Sets the owner to address(0), disabling all functions restricted to the owner. + */ + function renounceOwnership() external { + OwnerStorage storage s = getStorage(); + if (msg.sender != s.owner) { + revert OwnerUnauthorizedAccount(); + } + address previousOwner = s.owner; + s.owner = address(0); + emit OwnershipTransferred(previousOwner, address(0)); + } + + /** + * @notice Exports the function selectors of the OwnerRenounceFacet + * @dev This function is used as a selector discovery mechanism for diamonds. + * @return selectors The exported function selectors of the OwnerRenounceFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.renounceOwnership.selector); + } +} diff --git a/cli/project-template/src/templates/access/Owner/Renounce/OwnerRenounceMod.sol b/cli/project-template/src/templates/access/Owner/Renounce/OwnerRenounceMod.sol new file mode 100644 index 00000000..9efbb822 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/Renounce/OwnerRenounceMod.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-173 Renounce Ownership Module + * @notice Provides logic to renounce ownership. + */ + +/** + * @dev This emits when ownership of a contract changes. + */ +event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + +/** + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ +error OwnerUnauthorizedAccount(); + +bytes32 constant STORAGE_POSITION = keccak256("erc173.owner"); + +/** + * @custom:storage-location erc8042:erc173.owner + */ +struct OwnerStorage { + address owner; +} + +/** + * @notice Returns a pointer to the ERC-173 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ +function getStorage() pure returns (OwnerStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Renounce ownership of the contract. + * @dev Sets the owner to address(0), disabling all functions restricted to the owner. + */ +function renounceOwnership() { + OwnerStorage storage s = getStorage(); + if (msg.sender != s.owner) { + revert OwnerUnauthorizedAccount(); + } + address previousOwner = s.owner; + s.owner = address(0); + emit OwnershipTransferred(previousOwner, address(0)); +} + diff --git a/cli/project-template/src/templates/access/Owner/Transfer/OwnerTransferFacet.sol b/cli/project-template/src/templates/access/Owner/Transfer/OwnerTransferFacet.sol new file mode 100644 index 00000000..7e835a58 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/Transfer/OwnerTransferFacet.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-173 Contract Facet for Ownership Transfer + */ +contract OwnerTransferFacet { + /** + * @dev This emits when ownership of a contract changes. + */ + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ + error OwnerUnauthorizedAccount(); + + bytes32 constant STORAGE_POSITION = keccak256("erc173.owner"); + + /** + * @custom:storage-location erc8042:erc173.owner + */ + struct OwnerStorage { + address owner; + } + + /** + * @notice Returns a pointer to the owner storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ + function getStorage() internal pure returns (OwnerStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Set the address of the new owner of the contract. + * @dev Set _newOwner to address(0) to renounce any ownership. + * @param _newOwner The address of the new owner of the contract. + */ + function transferOwnership(address _newOwner) external { + OwnerStorage storage s = getStorage(); + if (msg.sender != s.owner) { + revert OwnerUnauthorizedAccount(); + } + address previousOwner = s.owner; + s.owner = _newOwner; + emit OwnershipTransferred(previousOwner, _newOwner); + } + + /** + * @notice Exports the function selectors of the OwnerTransferFacet + * @dev Used as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the OwnerTransferFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.transferOwnership.selector); + } +} diff --git a/cli/project-template/src/templates/access/Owner/Transfer/OwnerTransferMod.sol b/cli/project-template/src/templates/access/Owner/Transfer/OwnerTransferMod.sol new file mode 100644 index 00000000..dbaae6da --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/Transfer/OwnerTransferMod.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC-173 Ownership Transfer Module + * @notice Provides ownership transfer logic + */ + +/** + * @dev This emits when ownership of a contract changes. + */ +event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + +/* + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ +error OwnerUnauthorizedAccount(); + +bytes32 constant STORAGE_POSITION = keccak256("erc173.owner"); + +/** + * @custom:storage-location erc8042:erc173.owner + */ +struct OwnerStorage { + address owner; +} + +/** + * @notice Returns a pointer to the ERC-173 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ +function getStorage() pure returns (OwnerStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Set the address of the new owner of the contract. + * @dev Set _newOwner to address(0) to renounce any ownership. + * @param _newOwner The address of the new owner of the contract. + */ +function transferOwnership(address _newOwner) { + OwnerStorage storage s = getStorage(); + if (msg.sender != s.owner) { + revert OwnerUnauthorizedAccount(); + } + address previousOwner = s.owner; + s.owner = _newOwner; + emit OwnershipTransferred(previousOwner, _newOwner); +} diff --git a/cli/project-template/src/templates/access/Owner/TwoSteps/Data/OwnerTwoStepDataFacet.sol b/cli/project-template/src/templates/access/Owner/TwoSteps/Data/OwnerTwoStepDataFacet.sol new file mode 100644 index 00000000..4f07573e --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/TwoSteps/Data/OwnerTwoStepDataFacet.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title OwnerTwoStepDataFacet + */ +contract OwnerTwoStepDataFacet { + bytes32 constant STORAGE_POSITION = keccak256("erc173.owner.pending"); + + /** + * @custom:storage-location erc8042:erc173.owner.pending + */ + struct PendingOwnerStorage { + address pendingOwner; + } + + /** + * @notice Returns a pointer to the PendingOwner storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The PendingOwnerStorage struct in storage. + */ + function getStorage() internal pure returns (PendingOwnerStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Get the address of the pending owner + * @return The address of the pending owner. + */ + function pendingOwner() external view returns (address) { + return getStorage().pendingOwner; + } + + /** + * @notice Exports the function selectors of the OwnerTwoStepDataFacet + * @dev This function is used as a selector discovery mechanism for diamonds. + * @return selectors The exported function selectors of the OwnerTwoStepDataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.pendingOwner.selector); + } +} diff --git a/cli/project-template/src/templates/access/Owner/TwoSteps/Data/OwnerTwoStepDataMod.sol b/cli/project-template/src/templates/access/Owner/TwoSteps/Data/OwnerTwoStepDataMod.sol new file mode 100644 index 00000000..1690f405 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/TwoSteps/Data/OwnerTwoStepDataMod.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC-173 Two-Step Ownership Data Module + * @notice Provides data for two-step ownership transfer + */ + +bytes32 constant STORAGE_POSITION = keccak256("erc173.owner.pending"); + +/** + * @custom:storage-location erc8042:erc173.owner.pending + */ +struct PendingOwnerStorage { + address pendingOwner; +} + +/** + * @notice Returns a pointer to the PendingOwner storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The PendingOwnerStorage struct in storage. + */ +function getStorage() pure returns (PendingOwnerStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Get the address of the pending owner + * @return The address of the pending owner. + */ +function pendingOwner() view returns (address) { + return getStorage().pendingOwner; +} diff --git a/cli/project-template/src/templates/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceFacet.sol b/cli/project-template/src/templates/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceFacet.sol new file mode 100644 index 00000000..e9f5fc64 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceFacet.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title OwnerTwoStepRenounceFacet + */ +contract OwnerTwoStepRenounceFacet { + /** + * @dev This emits when ownership of a contract changes. + */ + event OwnershipTransferred(address indexed _previousOwner, address indexed _newOwner); + + /** + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ + error OwnerUnauthorizedAccount(); + + bytes32 constant OWNER_STORAGE_POSITION = keccak256("erc173.owner"); + + /** + * @custom:storage-location erc8042:erc173.owner + */ + struct OwnerStorage { + address owner; + } + + bytes32 constant PENDING_OWNER_STORAGE_POSITION = keccak256("erc173.owner.pending"); + + /** + * @custom:storage-location erc8042:erc173.owner.pending + */ + struct PendingOwnerStorage { + address pendingOwner; + } + + /** + * @notice Returns a pointer to the Owner storage struct. + * @dev Uses inline assembly to access the storage slot defined by OWNER_STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ + function getOwnerStorage() internal pure returns (OwnerStorage storage s) { + bytes32 position = OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns a pointer to the PendingOwner storage struct. + * @dev Uses inline assembly to access the storage slot defined by PENDING_OWNER_STORAGE_POSITION. + * @return s The PendingOwnerStorage struct in storage. + */ + function getPendingOwnerStorage() internal pure returns (PendingOwnerStorage storage s) { + bytes32 position = PENDING_OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Renounce ownership of the contract + * @dev Sets the owner to address(0), disabling all functions restricted to the owner. + */ + function renounceOwnership() external { + OwnerStorage storage ownerStorage = getOwnerStorage(); + PendingOwnerStorage storage pendingStorage = getPendingOwnerStorage(); + if (msg.sender != ownerStorage.owner) { + revert OwnerUnauthorizedAccount(); + } + address previousOwner = ownerStorage.owner; + ownerStorage.owner = address(0); + pendingStorage.pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, address(0)); + } + + /** + * @notice Exports the function selectors of the OwnerTwoStepRenounceFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the OwnerTwoStepRenounceFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.renounceOwnership.selector); + } +} diff --git a/cli/project-template/src/templates/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceMod.sol b/cli/project-template/src/templates/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceMod.sol new file mode 100644 index 00000000..369302ac --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceMod.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC-173 Two-Step Renounce Ownership Module + * @notice Provides logic to renounce ownership in a two-step ownership model. + */ + +/** + * @dev This emits when ownership of a contract changes. + */ +event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + +/* + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ +error OwnerUnauthorizedAccount(); + +bytes32 constant OWNER_STORAGE_POSITION = keccak256("erc173.owner"); + +/** + * @custom:storage-location erc8042:erc173.owner + */ +struct OwnerStorage { + address owner; +} + +bytes32 constant PENDING_OWNER_STORAGE_POSITION = keccak256("erc173.owner.pending"); + +/** + * @custom:storage-location erc8042:erc173.owner.pending + */ +struct PendingOwnerStorage { + address pendingOwner; +} + +/** + * @notice Returns a pointer to the ERC-173 storage struct. + * @dev Uses inline assembly to access the storage slot defined by OWNER_STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ +function getOwnerStorage() pure returns (OwnerStorage storage s) { + bytes32 position = OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns a pointer to the PendingOwner storage struct. + * @dev Uses inline assembly to access the storage slot defined by PENDING_OWNER_STORAGE_POSITION. + * @return s The PendingOwnerStorage struct in storage. + */ +function getPendingOwnerStorage() pure returns (PendingOwnerStorage storage s) { + bytes32 position = PENDING_OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Renounce ownership of the contract. + * @dev Sets the owner to address(0) and clears any pending owner, + * disabling all functions restricted to the owner. + */ +function renounceOwnership() { + OwnerStorage storage ownerStorage = getOwnerStorage(); + PendingOwnerStorage storage pendingStorage = getPendingOwnerStorage(); + if (msg.sender != ownerStorage.owner) { + revert OwnerUnauthorizedAccount(); + } + address previousOwner = ownerStorage.owner; + ownerStorage.owner = address(0); + pendingStorage.pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, address(0)); +} + diff --git a/cli/project-template/src/templates/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferFacet.sol b/cli/project-template/src/templates/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferFacet.sol new file mode 100644 index 00000000..b80f5126 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferFacet.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title OwnerTwoStepTransferFacet + */ +contract OwnerTwoStepTransferFacet { + /** + * @dev This emits when ownership of a contract started transferring to the new owner for accepting the ownership. + */ + event OwnershipTransferStarted(address indexed _previousOwner, address indexed _newOwner); + + /** + * @dev This emits when ownership of a contract changes. + */ + event OwnershipTransferred(address indexed _previousOwner, address indexed _newOwner); + + /** + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ + error OwnerUnauthorizedAccount(); + + bytes32 constant OWNER_STORAGE_POSITION = keccak256("erc173.owner"); + + /** + * @custom:storage-location erc8042:erc173.owner + */ + struct OwnerStorage { + address owner; + } + + bytes32 constant PENDING_OWNER_STORAGE_POSITION = keccak256("erc173.owner.pending"); + + /** + * @custom:storage-location erc8042:erc173.owner.pending + */ + struct PendingOwnerStorage { + address pendingOwner; + } + + /** + * @notice Returns a pointer to the Owner storage struct. + * @dev Uses inline assembly to access the storage slot defined by OWNER_STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ + function getOwnerStorage() internal pure returns (OwnerStorage storage s) { + bytes32 position = OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns a pointer to the PendingOwner storage struct. + * @dev Uses inline assembly to access the storage slot defined by PENDING_OWNER_STORAGE_POSITION. + * @return s The PendingOwnerStorage struct in storage. + */ + function getPendingOwnerStorage() internal pure returns (PendingOwnerStorage storage s) { + bytes32 position = PENDING_OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Set the address of the new owner of the contract + * @param _newOwner The address of the new owner of the contract + */ + function transferOwnership(address _newOwner) external { + OwnerStorage storage ownerStorage = getOwnerStorage(); + if (msg.sender != ownerStorage.owner) { + revert OwnerUnauthorizedAccount(); + } + getPendingOwnerStorage().pendingOwner = _newOwner; + emit OwnershipTransferStarted(ownerStorage.owner, _newOwner); + } + + /** + * @notice Accept the ownership of the contract + * @dev Only the pending owner can call this function. + */ + function acceptOwnership() external { + OwnerStorage storage ownerStorage = getOwnerStorage(); + PendingOwnerStorage storage pendingStorage = getPendingOwnerStorage(); + if (msg.sender != pendingStorage.pendingOwner) { + revert OwnerUnauthorizedAccount(); + } + address previousOwner = ownerStorage.owner; + ownerStorage.owner = pendingStorage.pendingOwner; + pendingStorage.pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, ownerStorage.owner); + } + + /** + * @notice Exports the function selectors of the OwnerTwoStepTransferFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the OwnerTwoStepTransferFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.transferOwnership.selector, this.acceptOwnership.selector); + } +} diff --git a/cli/project-template/src/templates/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferMod.sol b/cli/project-template/src/templates/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferMod.sol new file mode 100644 index 00000000..b4c45998 --- /dev/null +++ b/cli/project-template/src/templates/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferMod.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC-173 Two-Step Ownership Transfer Module + * @notice Provides logic for two-step ownership transfers. + */ + +/** + * @dev Emitted when ownership transfer is initiated (pending owner set). + */ +event OwnershipTransferStarted(address indexed _previousOwner, address indexed _newOwner); + +/** + * @dev Emitted when ownership transfer is finalized. + */ +event OwnershipTransferred(address indexed _previousOwner, address indexed _newOwner); + +/* + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ +error OwnerUnauthorizedAccount(); + +bytes32 constant OWNER_STORAGE_POSITION = keccak256("erc173.owner"); + +/** + * @custom:storage-location erc8042:erc173.owner + */ +struct OwnerStorage { + address owner; +} + +bytes32 constant PENDING_OWNER_STORAGE_POSITION = keccak256("erc173.owner.pending"); + +/** + * @custom:storage-location erc8042:erc173.owner.pending + */ +struct PendingOwnerStorage { + address pendingOwner; +} + +/** + * @notice Returns a pointer to the Owner storage struct. + * @dev Uses inline assembly to access the storage slot defined by OWNER_STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ +function getOwnerStorage() pure returns (OwnerStorage storage s) { + bytes32 position = OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns a pointer to the PendingOwner storage struct. + * @dev Uses inline assembly to access the storage slot defined by PENDING_OWNER_STORAGE_POSITION. + * @return s The PendingOwnerStorage struct in storage. + */ +function getPendingOwnerStorage() pure returns (PendingOwnerStorage storage s) { + bytes32 position = PENDING_OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Initiates a two-step ownership transfer. + * @param _newOwner The address of the new owner of the contract. + */ +function transferOwnership(address _newOwner) { + OwnerStorage storage ownerStorage = getOwnerStorage(); + if (msg.sender != ownerStorage.owner) { + revert OwnerUnauthorizedAccount(); + } + getPendingOwnerStorage().pendingOwner = _newOwner; + emit OwnershipTransferStarted(ownerStorage.owner, _newOwner); +} + +/** + * @notice Finalizes ownership transfer. + * @dev Only the pending owner can call this function. + */ +function acceptOwnership() { + OwnerStorage storage ownerStorage = getOwnerStorage(); + PendingOwnerStorage storage pendingStorage = getPendingOwnerStorage(); + if (msg.sender != pendingStorage.pendingOwner) { + revert OwnerUnauthorizedAccount(); + } + address previousOwner = ownerStorage.owner; + ownerStorage.owner = pendingStorage.pendingOwner; + pendingStorage.pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, ownerStorage.owner); +} + diff --git a/cli/project-template/src/templates/diamond/DiamondInspectFacet.sol b/cli/project-template/src/templates/diamond/DiamondInspectFacet.sol new file mode 100644 index 00000000..4f1c4490 --- /dev/null +++ b/cli/project-template/src/templates/diamond/DiamondInspectFacet.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +interface IFacet { + function exportSelectors() external view returns (bytes memory); +} + +contract DiamondInspectFacet { + bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("erc8153.diamond"); + + struct FacetNode { + address facet; + bytes4 prevFacetNodeId; + bytes4 nextFacetNodeId; + } + + struct FacetList { + bytes4 headFacetNodeId; + bytes4 tailFacetNodeId; + uint32 facetCount; + uint32 selectorCount; + } + + /** + * @custom:storage-location erc8042:erc8153.diamond + */ + struct DiamondStorage { + mapping(bytes4 functionSelector => FacetNode) facetNodes; + FacetList facetList; + } + + function getStorage() internal pure returns (DiamondStorage storage s) { + bytes32 position = DIAMOND_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Gets the facet address that handles the given selector. + * @dev If facet is not found return address(0). + * @param _functionSelector The function selector. + * @return facet The facet address. + */ + function facetAddress(bytes4 _functionSelector) external view returns (address facet) { + DiamondStorage storage s = getStorage(); + facet = s.facetNodes[_functionSelector].facet; + } + + /** + * @notice Decodes a packed bytes array into a standard bytes4[] array. + * @param packed The packed bytes (e.g., from `bytes.concat`). + * @return unpacked The standard padded bytes4[] array. + */ + function unpackSelectors(bytes memory packed) internal pure returns (bytes4[] memory unpacked) { + /* + * Allocate the output array + */ + uint256 count = packed.length / 4; + unpacked = new bytes4[](count); + /* + * Copy from packed to unpacked + */ + assembly ("memory-safe") { + /* + * 'src' points to the start of the data in the packed array (skip 32-byte length) + */ + let src := add(packed, 32) + /* + * 'dst' points to the start of the data in the new selectors array (skip 32-byte length) + */ + let dst := add(unpacked, 32) + /* + * 'end' is the stopping point for the destination pointer + */ + let end := add(dst, mul(count, 32)) + /* + * While 'dst' is less than 'end', keep copying + */ + for {} lt(dst, end) {} { + /* + * A. Load 32 bytes from the packed source. + * We read "dirty" data (neighboring bytes), but it doesn't matter + * because we truncate it when writing. + */ + let value := mload(src) + /* + * B. Clearn up the value to extract only the 4 bytes we want. + */ + value := and(value, 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000) + /* + * C. Store the value into the destination + */ + mstore(dst, value) + /* + * D. Advance pointers + */ + src := add(src, 4) // Move forward 4 bytes in packed source + dst := add(dst, 32) // Move forward 32 bytes in destination target + } + } + } + + /** + * @notice Gets the function selectors that are handled by the given facet. + * @dev If facet is not found return empty array. + * @param _facet The facet address. + * @return facetSelectors The function selectors. + */ + function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetSelectors) { + DiamondStorage storage s = getStorage(); + facetSelectors = unpackSelectors(IFacet(_facet).exportSelectors()); + if (facetSelectors.length == 0 || s.facetNodes[facetSelectors[0]].facet == address(0)) { + facetSelectors = new bytes4[](0); + } + } + + /** + * @notice Gets the facet addresses used by the diamond. + * @dev If no facets are registered return empty array. + * @return allFacets The facet addresses. + */ + function facetAddresses() external view returns (address[] memory allFacets) { + DiamondStorage storage s = getStorage(); + FacetList memory facetList = s.facetList; + allFacets = new address[](facetList.facetCount); + bytes4 currentSelector = facetList.headFacetNodeId; + for (uint256 i; i < facetList.facetCount; i++) { + address facet = s.facetNodes[currentSelector].facet; + allFacets[i] = facet; + currentSelector = s.facetNodes[currentSelector].nextFacetNodeId; + } + } + + struct Facet { + address facet; + bytes4[] functionSelectors; + } + + /** + * @notice Returns the facet address and function selectors of all facets + * in the diamond. + * @return facetsAndSelectors An array of Facet structs containing each + * facet address and its function selectors. + */ + function facets() external view returns (Facet[] memory facetsAndSelectors) { + DiamondStorage storage s = getStorage(); + FacetList memory facetList = s.facetList; + bytes4 currentSelector = facetList.headFacetNodeId; + facetsAndSelectors = new Facet[](facetList.facetCount); + for (uint256 i; i < facetList.facetCount; i++) { + address facet = s.facetNodes[currentSelector].facet; + bytes4[] memory facetSelectors = unpackSelectors(IFacet(facet).exportSelectors()); + facetsAndSelectors[i].facet = facet; + facetsAndSelectors[i].functionSelectors = facetSelectors; + currentSelector = s.facetNodes[currentSelector].nextFacetNodeId; + } + } + + /** + * @notice Exports the function selectors of the DiamondInspectFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the DiamondInspectFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat( + this.facetAddress.selector, + this.facetFunctionSelectors.selector, + this.facetAddresses.selector, + this.facets.selector + ); + } +} diff --git a/cli/project-template/src/templates/diamond/DiamondMod.sol b/cli/project-template/src/templates/diamond/DiamondMod.sol new file mode 100644 index 00000000..4cb090fb --- /dev/null +++ b/cli/project-template/src/templates/diamond/DiamondMod.sol @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* +* @title Diamond Module +* @notice Internal functions and storage for diamond proxy functionality. +* @dev Follows EIP-8153 Facet-Based Diamonds +*/ + +interface IFacet { + function exportSelectors() external pure returns (bytes memory); +} + +bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("erc8153.diamond"); + +struct FacetNode { + address facet; + bytes4 prevFacetNodeId; + bytes4 nextFacetNodeId; +} + +struct FacetList { + bytes4 headFacetNodeId; + bytes4 tailFacetNodeId; + uint32 facetCount; + uint32 selectorCount; +} + +/** + * @custom:storage-location erc8042:erc8153.diamond + */ +struct DiamondStorage { + mapping(bytes4 functionSelector => FacetNode) facetNodes; + FacetList facetList; +} + +function getDiamondStorage() pure returns (DiamondStorage storage s) { + bytes32 position = DIAMOND_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Emitted when a facet is added to a diamond. + * @dev The function selectors this facet handles can be retrieved by calling + * `IFacet(_facet).exportSelectors()` + * + * @param _facet The address of the facet that handles function calls to the diamond. + */ +event FacetAdded(address indexed _facet); + +/** + * @notice Emitted when an existing facet is replaced with a new facet. + * @dev + * - Selectors that are present in the new facet but not in the old facet are added to the diamond. + * - Selectors that are present in both the new and old facet are updated to use the new facet. + * - Selectors that are not present in the new facet but are present in the old facet are removed from + * the diamond. + * + * The function selectors handled by these facets can be retrieved by calling: + * - `IFacet(_oldFacet).exportSelectors()` + * - `IFacet(_newFacet).exportSelectors()` + * + * @param _oldFacet The address of the facet that previously handled function calls to the diamond. + * @param _newFacet The address of the facet that now handles function calls to the diamond. + */ +event FacetReplaced(address indexed _oldFacet, address indexed _newFacet); + +/** + * @notice Emitted when a facet is removed from a diamond. + * @dev The function selectors this facet handles can be retrieved by calling + * `IFacet(_facet).exportSelectors()` + * + * @param _facet The address of the facet that previouly handled function calls to the diamond. + */ +event FacetRemoved(address indexed _facet); + +/** + * @notice Emitted when a diamond's constructor function or function from a + * facet makes a `delegatecall`. + * + * @param _delegate The contract that was delegatecalled. + * @param _delegateCalldata The function call, including function selector and + * any arguments. + */ +event DiamondDelegateCall(address indexed _delegate, bytes _delegateCalldata); + +/** + * @notice Emitted to record information about a diamond. + * @dev This event records any arbitrary metadata. + * The format of `_tag` and `_data` are not specified by the + * standard. + * + * @param _tag Arbitrary metadata, such as a release version. + * @param _data Arbitrary metadata. + */ +event DiamondMetadata(bytes32 indexed _tag, bytes _data); + +/** + * @notice The upgradeDiamond function below detects and reverts + * with the following errors. + */ +error NoSelectorsForFacet(address _facet); +error NoBytecodeAtAddress(address _contractAddress); +error CannotAddFunctionToDiamondThatAlreadyExists(bytes4 _selector); +error FunctionSelectorsCallFailed(address _facet); +error IncorrectSelectorsEncoding(address _facet); + +function importSelectors(address _facet) view returns (bytes memory selectors) { + (bool success, bytes memory data) = _facet.staticcall(abi.encodeWithSelector(IFacet.exportSelectors.selector)); + if (success == false) { + revert FunctionSelectorsCallFailed(_facet); + } + /* + * Ensure the data is large enough. + * Offset (32 bytes) + array length (32 bytes) + */ + if (data.length < 64) { + if (_facet.code.length == 0) { + revert NoBytecodeAtAddress(_facet); + } else { + revert IncorrectSelectorsEncoding(_facet); + } + } + + /* Validate ABI offset == 0x20 for a single dynamic return */ + uint256 offset; + assembly ("memory-safe") { + offset := mload(add(data, 0x20)) + } + if (offset != 0x20) { + revert IncorrectSelectorsEncoding(_facet); + } + /* + * ZERO-COPY DECODE + * Instead of abi.decode(wrapper, (bytes)), which copies memory, + * we use assembly to point 'selectors' to the bytes array inside 'data'. + * The length of `data` is stored at 0 and an ABI offset is located at 0x20 (32). + * We skip over those to point `selectors` to the length of the + * bytes array. + */ + assembly ("memory-safe") { + selectors := add(data, 0x40) + } + uint256 selectorsLength = selectors.length; + unchecked { + if (selectorsLength > data.length - 64) { + revert IncorrectSelectorsEncoding(_facet); + } + } + if (selectorsLength < 4) { + revert NoSelectorsForFacet(_facet); + } + /* + * Function selectors are strictly 4 bytes. We ensure the length is a multiple of 4. + */ + if (selectorsLength % 4 != 0) { + revert IncorrectSelectorsEncoding(_facet); + } + return selectors; +} + +function at(bytes memory selectors, uint256 index) pure returns (bytes4 selector) { + assembly ("memory-safe") { + /** + * 1. Calculate Pointer + * add(selectors, 32) - skips the length field of the bytes array + * shl(2, index) is the same as index * 4 but cheaper + * This line executes: ptr = selectorsLength + (4 * index) + */ + let ptr := add(add(selectors, 32), shl(2, index)) + /** + * 2. Load & Return + * We load 32 bytes, but Solidity truncates to 4 bytes automatically + * upon return of this function, so masking is unnecessary. + */ + selector := mload(ptr) + } +} + +function addFacets(address[] memory _facets) { + DiamondStorage storage s = getDiamondStorage(); + uint256 facetLength = _facets.length; + if (facetLength == 0) { + return; + } + FacetList memory facetList = s.facetList; + /* + * Snapshot free memory pointer. We restore this at the end of every loop + * to prevent memory expansion costs from repeated `packedSelectors` calls. + */ + uint256 freeMemPtr; + assembly ("memory-safe") { + freeMemPtr := mload(0x40) + } + /* Algorithm Description: + * The first facet is handled separately to initialize the linked list pointers in the FacetNodes. + * This allows us to avoid additional conditional checks for linked list management in the main facet loop. + * + * For the first facet, we link the first selector to the previous facet or if this is the first facet in + * the diamond then we assign the first selector to facetList.firstFacetNodeId. + * + * All the selectors (except the first one) in the first facet are then added to the diamond. + * + * In the first iteration of the main facet loop the the selectors for the next facet are retrieved. + * This makes available the nextFacetNodeId value that is needed to store the first selector of the + * first facet. So then the first selector is stored. + * + * Then the selectors which were already retrieved for the next facet are stored, except the first selector. + * Then in the next iteration the selectors of the next facet are retrieved. This makes available the nextFacetNodeId + * value that is needed to store the first selector of the previous facet. The first selector is then stored. The loop + * continues. + * + * After the main facet loop ends, the first selector from the last facet is added to the diamond. + */ + + bytes4 prevFacetNodeId = facetList.tailFacetNodeId; + address facet = _facets[0]; + bytes memory selectors = importSelectors(facet); + /* + * currentFacetNodeId is the head node of the current facet. + * We cannot write it to storage yet because we don't know the `next` pointer. + */ + bytes4 currentFacetNodeId = at(selectors, 0); + if (facetList.facetCount == 0) { + facetList.headFacetNodeId = currentFacetNodeId; + } else { + /* + * Link the previous tail of the diamond to this new batch + */ + s.facetNodes[prevFacetNodeId].nextFacetNodeId = currentFacetNodeId; + } + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors + */ + uint256 selectorsLength = selectors.length >> 2; + unchecked { + facetList.selectorCount += uint32(selectorsLength); + } + /* + * Add all selectors, except the first, to the diamond. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(selectors, selectorIndex); + if (s.facetNodes[selector].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(selector); + } + s.facetNodes[selector] = FacetNode(facet, bytes4(0), bytes4(0)); + } + /* + * Reset memory for the main loop. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + /* + * Main facet loop. + * 1. Gets the next facet's selectors. + * 2. Now that the nextFacetNodeId value for the previous facet is available, adds the previous + * facet's first selector to the diamond. + * 3. Emits FacetAdded event for the previous facet. + * 3. Updates facet values: facet = nextFacet, etc. + * 4. Adds all the selectors (except the first) to the diamond. + * 5. Repeat loop. + */ + for (uint256 i = 1; i < facetLength; i++) { + address nextFacet = _facets[i]; + selectors = importSelectors(nextFacet); + /* + * Check to see if the PENDING first selector (from previous iteration) already exists in the diamond. + */ + if (s.facetNodes[currentFacetNodeId].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(currentFacetNodeId); + } + /* + * Identify the link to the next facet + */ + bytes4 nextFacetNodeId = at(selectors, 0); + /* + * Store the previous facet's first selector. + */ + s.facetNodes[currentFacetNodeId] = FacetNode(facet, prevFacetNodeId, nextFacetNodeId); + emit FacetAdded(facet); + /* + * Move pointers forward. + * These assignments switch us from processing the previous facet's first selector to + * processing the next facet's selectors. + * `currentFacetNodeId` becomes the new pending first selector. + */ + facet = nextFacet; + prevFacetNodeId = currentFacetNodeId; + currentFacetNodeId = nextFacetNodeId; + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors. + */ + selectorsLength = selectors.length >> 2; + /* + * Add all the selectors of the facet to the diamond, except the first selector. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(selectors, selectorIndex); + if (s.facetNodes[selector].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(selector); + } + s.facetNodes[selector] = FacetNode(facet, bytes4(0), bytes4(0)); + } + unchecked { + facetList.selectorCount += uint32(selectorsLength); + } + /* + * Restore Free Memory Pointer to reuse memory from packedSelectors() calls. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + } + /* + * Validates and adds the first selector of the last facet to the diamond. + */ + if (s.facetNodes[currentFacetNodeId].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(currentFacetNodeId); + } + s.facetNodes[currentFacetNodeId] = FacetNode(facet, prevFacetNodeId, bytes4(0)); + emit FacetAdded(facet); + facetList.facetCount += uint32(facetLength); + + facetList.tailFacetNodeId = currentFacetNodeId; + s.facetList = facetList; +} + +error FunctionNotFound(bytes4 _selector); + +/** + * Find facet for function that is called and execute the + * function if a facet is found and return any value. + */ +function diamondFallback() { + DiamondStorage storage s = getDiamondStorage(); + /** + * get facet from function selector + */ + address facet = s.facetNodes[msg.sig].facet; + if (facet == address(0)) { + revert FunctionNotFound(msg.sig); + } + /* + * Execute external function from facet using delegatecall and return any value. + */ + assembly { + /* + * copy function selector and any arguments + */ + calldatacopy(0, 0, calldatasize()) + /* + * execute function call using the facet + */ + let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) + /* + * get any return value + */ + returndatacopy(0, 0, returndatasize()) + /* + * return any return value or error back to the caller + */ + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } +} diff --git a/cli/project-template/src/templates/diamond/DiamondUpgradeFacet.sol b/cli/project-template/src/templates/diamond/DiamondUpgradeFacet.sol new file mode 100644 index 00000000..dfde194e --- /dev/null +++ b/cli/project-template/src/templates/diamond/DiamondUpgradeFacet.sol @@ -0,0 +1,725 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title Reference implementation for upgrade function for + * ERC-8153 Facet-Based Diamonds + * + * @dev + * Facets are stored as a doubly linked list and as a mapping of selectors to facet addresses. + * + * Facets are stored as a mapping of selectors to facet addresses for efficient delegatecall + * routing to facets. + * + * Facets are stored as a doubly linked list for efficient iteration over all facets, + * and for efficiently adding, replacing, and removing them. + * + * The `FacetList` struct contains information about the linked list of facets. + * + * Only the first FacetNode of each facet contains linked list pointers. + * * prevFacetNodeId - Is the selector of the first FacetNode of the previous + * facet. + * * nextFacetNodeId - Is the selector of the first FacetNode of the next + * facet. + * + * Here is a example that shows the structure: + * + * FacetList + * facetCount = 3 + * headFacetNodeId = selector1 // facetA + * tailFacetNodeId = selector7 // facetC + * + * facetNodes mapping (selector => FacetNode) + * + * selector facet prevFacetNodeId nextFacetNodeId + * ---------------------------------------------------------------- + * selector1 facetA 0x00000000 selector4 ← facetA LIST NODE + * selector2 facetA 0x00000000 0x00000000 + * selector3 facetA 0x00000000 0x00000000 + * + * selector4 facetB selector1 selector7 ← facetB LIST NODE + * selector5 facetB 0x00000000 0x00000000 + * selector6 facetB 0x00000000 0x00000000 + * + * selector7 facetC selector4 0x00000000 ← facetC LIST NODE + * selector8 facetC 0x00000000 0x00000000 + * selector9 facetC 0x00000000 0x00000000 + * + * Linked list order of facets: + * + * facetA (selector1) + * ↓ + * facetB (selector4) + * ↓ + * facetC (selector7) + * + * Notes: + * - Only the first selector of each facet participates in the linked list. + * - The linked list connects facets, not individual selectors. + * - Any values in "prevFacetNodeId" in non-first FacetNodes are not used. + * + * Checked/unchecked math note: + * We use unchecked math with `facetList.selectorCount` because that variable does not affect the adding, + * replacing, and removing of selectors and facets. It is used by some introspection functions. Checked math + * is used with facetList.facetCount because that affects adding, replacing, and removing selectors and facets. + * Of course these variables should never overflow/underflow anyway. + * + * Security: + * This implementation relies on the assumption that the owner of the diamond that has added or replaced any + * facet in the diamond has verified that each facet is not malicious, that each facet is immutable (not upgradeable), + * and that the `exportSelectors()` function in each facet is pure (is marked as a pure function and does not access state.) + */ + +interface IFacet { + function exportSelectors() external pure returns (bytes memory); +} + +contract DiamondUpgradeFacet { + /** + * @notice Thrown when a non-owner attempts an action restricted to owner. + */ + error OwnerUnauthorizedAccount(); + + bytes32 constant OWNER_STORAGE_POSITION = keccak256("erc173.owner"); + + /** + * @custom:storage-location erc8042:erc173.owner + */ + struct OwnerStorage { + address owner; + } + + /** + * @notice Returns a pointer to the owner storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The OwnerStorage struct in storage. + */ + function getOwnerStorage() internal pure returns (OwnerStorage storage s) { + bytes32 position = OWNER_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("erc8153.diamond"); + + struct FacetNode { + address facet; + bytes4 prevFacetNodeId; + bytes4 nextFacetNodeId; + } + + struct FacetList { + bytes4 headFacetNodeId; + bytes4 tailFacetNodeId; + uint32 facetCount; + uint32 selectorCount; + } + + /** + * @custom:storage-location erc8042:erc8153.diamond + */ + struct DiamondStorage { + mapping(bytes4 functionSelector => FacetNode) facetNodes; + FacetList facetList; + } + + function getDiamondStorage() internal pure returns (DiamondStorage storage s) { + bytes32 position = DIAMOND_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Emitted when a facet is added to a diamond. + * @dev The function selectors this facet handles can be retrieved by calling + * `IFacet(_facet).exportSelectors()` + * + * @param _facet The address of the facet that handles function calls to the diamond. + */ + event FacetAdded(address indexed _facet); + + /** + * @notice Emitted when an existing facet is replaced with a new facet. + * @dev + * - Selectors that are present in the new facet but not in the old facet are added to the diamond. + * - Selectors that are present in both the new and old facet are updated to use the new facet. + * - Selectors that are not present in the new facet but are present in the old facet are removed from + * the diamond. + * + * The function selectors handled by these facets can be retrieved by calling: + * - `IFacet(_oldFacet).exportSelectors()` + * - `IFacet(_newFacet).exportSelectors()` + * + * @param _oldFacet The address of the facet that previously handled function calls to the diamond. + * @param _newFacet The address of the facet that now handles function calls to the diamond. + */ + event FacetReplaced(address indexed _oldFacet, address indexed _newFacet); + + /** + * @notice Emitted when a facet is removed from a diamond. + * @dev The function selectors this facet handles can be retrieved by calling + * `IFacet(_facet).exportSelectors()` + * + * @param _facet The address of the facet that previously handled function calls to the diamond. + */ + event FacetRemoved(address indexed _facet); + + /** + * @notice Emitted when a diamond's constructor function or function from a + * facet makes a `delegatecall`. + * + * @param _delegate The contract that was delegatecalled. + * @param _delegateCalldata The function call, including function selector and + * any arguments. + */ + event DiamondDelegateCall(address indexed _delegate, bytes _delegateCalldata); + + /** + * @notice Emitted to record information about a diamond. + * @dev This event records any arbitrary metadata. + * The format of `_tag` and `_data` are not specified by the + * standard. + * + * @param _tag Arbitrary metadata, such as a release version. + * @param _data Arbitrary metadata. + */ + event DiamondMetadata(bytes32 indexed _tag, bytes _data); + + /** + * @notice The upgradeDiamond function below detects and reverts + * with the following errors. + */ + error NoSelectorsForFacet(address _facet); + error NoBytecodeAtAddress(address _contractAddress); + error CannotAddFunctionToDiamondThatAlreadyExists(bytes4 _selector); + error CannotRemoveFacetThatDoesNotExist(address _facet); + error CannotReplaceFacetWithSameFacet(address _facet); + error FacetToReplaceDoesNotExist(address _oldFacet); + error DelegateCallReverted(address _delegate, bytes _delegateCalldata); + error ExportSelectorsCallFailed(address _facet); + error IncorrectSelectorsEncoding(address _facet); + + /** + * @dev This error means that a function to replace exists in a + * facet other than the facet that was given to be replaced. + */ + error CannotReplaceFunctionFromNonReplacementFacet(bytes4 _selector); + + function importSelectors(address _facet) internal view returns (bytes memory selectors) { + (bool success, bytes memory data) = _facet.staticcall(abi.encodeWithSelector(IFacet.exportSelectors.selector)); + if (success == false) { + revert ExportSelectorsCallFailed(_facet); + } + /* + * Ensure the data is large enough. + * Offset (32 bytes) + array length (32 bytes) + */ + if (data.length < 64) { + if (_facet.code.length == 0) { + revert NoBytecodeAtAddress(_facet); + } else { + revert IncorrectSelectorsEncoding(_facet); + } + } + + /* Validate ABI offset == 0x20 for a single dynamic return */ + uint256 offset; + assembly ("memory-safe") { + offset := mload(add(data, 0x20)) + } + if (offset != 0x20) { + revert IncorrectSelectorsEncoding(_facet); + } + /* + * ZERO-COPY DECODE + * Instead of abi.decode(wrapper, (bytes)), which copies memory, + * we use assembly to point 'selectors' to the bytes array inside 'data'. + * The length of `data` is stored at 0 and an ABI offset is located at 0x20 (32). + * We skip over those to point `selectors` to the length of the + * bytes array. + */ + assembly ("memory-safe") { + selectors := add(data, 0x40) + } + uint256 selectorsLength = selectors.length; + unchecked { + if (selectorsLength > data.length - 64) { + revert IncorrectSelectorsEncoding(_facet); + } + } + if (selectorsLength < 4) { + revert NoSelectorsForFacet(_facet); + } + /* + * Function selectors are strictly 4 bytes. We ensure the length is a multiple of 4. + */ + if (selectorsLength % 4 != 0) { + revert IncorrectSelectorsEncoding(_facet); + } + return selectors; + } + + function at(bytes memory selectors, uint256 index) internal pure returns (bytes4 selector) { + assembly ("memory-safe") { + /** + * 1. Calculate Pointer + * add(selectors, 32) - skips the length field of the bytes array + * shl(2, index) is the same as index * 4 but cheaper + * This line executes: ptr = selectorsLength + (4 * index) + */ + let ptr := add(add(selectors, 32), shl(2, index)) + /** + * 2. Load & Return + * We load 32 bytes, but Solidity truncates to 4 bytes automatically + * upon return of this function, so masking is unnecessary. + */ + selector := mload(ptr) + } + } + + function addFacets(address[] calldata _facets) internal { + DiamondStorage storage s = getDiamondStorage(); + uint256 facetLength = _facets.length; + if (facetLength == 0) { + return; + } + FacetList memory facetList = s.facetList; + /* + * Snapshot free memory pointer. We restore this at the end of every loop + * to prevent memory expansion costs from repeated `packedSelectors` calls. + */ + uint256 freeMemPtr; + assembly ("memory-safe") { + freeMemPtr := mload(0x40) + } + /* Algorithm Description: + * The first facet is handled separately to initialize the linked list pointers in the FacetNodes. + * This allows us to avoid additional conditional checks for linked list management in the main facet loop. + * + * For the first facet, we link the first selector to the previous facet or if this is the first facet in + * the diamond then we assign the first selector to facetList.firstFacetNodeId. + * + * All the selectors (except the first one) in the first facet are then added to the diamond. + * + * In the first iteration of the main facet loop the the selectors for the next facet are retrieved. + * This makes available the nextFacetNodeId value that is needed to store the first selector of the + * first facet. So then the first selector is stored. + * + * Then the selectors which were already retrieved for the next facet are stored, except the first selector. + * Then in the next iteration the selectors of the next facet are retrieved. This makes available the nextFacetNodeId + * value that is needed to store the first selector of the previous facet. The first selector is then stored. The loop + * continues. + * + * After the main facet loop ends, the first selector from the last facet is added to the diamond. + */ + + bytes4 prevFacetNodeId = facetList.tailFacetNodeId; + address facet = _facets[0]; + bytes memory selectors = importSelectors(facet); + /* + * currentFacetNodeId is the head node of the current facet. + * We cannot write it to storage yet because we don't know the `next` pointer. + */ + bytes4 currentFacetNodeId = at(selectors, 0); + if (facetList.facetCount == 0) { + facetList.headFacetNodeId = currentFacetNodeId; + } else { + /* + * Link the previous tail of the diamond to this new batch + */ + s.facetNodes[prevFacetNodeId].nextFacetNodeId = currentFacetNodeId; + } + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors + */ + uint256 selectorsLength = selectors.length >> 2; + unchecked { + facetList.selectorCount += uint32(selectorsLength); + } + /* + * Add all selectors, except the first, to the diamond. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(selectors, selectorIndex); + if (s.facetNodes[selector].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(selector); + } + s.facetNodes[selector] = FacetNode(facet, bytes4(0), bytes4(0)); + } + /* + * Reset memory for the main loop. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + /* + * Main facet loop. + * 1. Gets the next facet's selectors. + * 2. Now that the nextFacetNodeId value for the previous facet is available, adds the previous + * facet's first selector to the diamond. + * 3. Emits FacetAdded event for the previous facet. + * 3. Updates facet values: facet = nextFacet, etc. + * 4. Adds all the selectors (except the first) to the diamond. + * 5. Repeat loop. + */ + for (uint256 i = 1; i < facetLength; i++) { + address nextFacet = _facets[i]; + selectors = importSelectors(nextFacet); + /* + * Check to see if the PENDING first selector (from previous iteration) already exists in the diamond. + */ + if (s.facetNodes[currentFacetNodeId].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(currentFacetNodeId); + } + /* + * Identify the link to the next facet + */ + bytes4 nextFacetNodeId = at(selectors, 0); + /* + * Store the previous facet's first selector. + */ + s.facetNodes[currentFacetNodeId] = FacetNode(facet, prevFacetNodeId, nextFacetNodeId); + emit FacetAdded(facet); + /* + * Move pointers forward. + * These assignments switch us from processing the previous facet's first selector to + * processing the next facet's selectors. + * `currentFacetNodeId` becomes the new pending first selector. + */ + facet = nextFacet; + prevFacetNodeId = currentFacetNodeId; + currentFacetNodeId = nextFacetNodeId; + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors. + */ + selectorsLength = selectors.length >> 2; + /* + * Add all the selectors of the facet to the diamond, except the first selector. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(selectors, selectorIndex); + if (s.facetNodes[selector].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(selector); + } + s.facetNodes[selector] = FacetNode(facet, bytes4(0), bytes4(0)); + } + unchecked { + facetList.selectorCount += uint32(selectorsLength); + } + /* + * Restore Free Memory Pointer to reuse memory from packedSelectors() calls. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + } + /* + * Validates and adds the first selector of the last facet to the diamond. + */ + if (s.facetNodes[currentFacetNodeId].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(currentFacetNodeId); + } + s.facetNodes[currentFacetNodeId] = FacetNode(facet, prevFacetNodeId, bytes4(0)); + emit FacetAdded(facet); + facetList.facetCount += uint32(facetLength); + + facetList.tailFacetNodeId = currentFacetNodeId; + s.facetList = facetList; + } + + /** + * @notice This struct is used to replace old facets with new facets. + */ + struct FacetReplacement { + address oldFacet; + address newFacet; + } + + function replaceFacets(FacetReplacement[] calldata _replaceFacets) internal { + DiamondStorage storage s = getDiamondStorage(); + FacetList memory facetList = s.facetList; + /* + * Snapshot free memory pointer. We restore this within the loop to prevent + * memory expansion costs from repeated `packedSelectors` calls. + */ + uint256 freeMemPtr; + assembly ("memory-safe") { + freeMemPtr := mload(0x40) + } + for (uint256 i; i < _replaceFacets.length; i++) { + address oldFacet = _replaceFacets[i].oldFacet; + address newFacet = _replaceFacets[i].newFacet; + if (oldFacet == newFacet) { + revert CannotReplaceFacetWithSameFacet(oldFacet); + } + bytes memory oldSelectors = importSelectors(oldFacet); + bytes memory newSelectors = importSelectors(newFacet); + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors + */ + uint256 selectorsLength = newSelectors.length >> 2; + bytes4 oldCurrentFacetNodeId = at(oldSelectors, 0); + bytes4 newCurrentFacetNodeId = at(newSelectors, 0); + + /** + * Validate old facet exists. + */ + FacetNode memory oldFacetNode = s.facetNodes[oldCurrentFacetNodeId]; + if (oldFacetNode.facet != oldFacet) { + revert FacetToReplaceDoesNotExist(oldFacet); + } + if (oldCurrentFacetNodeId != newCurrentFacetNodeId) { + /** + * Write first selector with linking info, then process remaining. + */ + address existingFacet = s.facetNodes[newCurrentFacetNodeId].facet; + if (existingFacet == address(0)) { + unchecked { + facetList.selectorCount++; + } + } else if (existingFacet != oldFacet) { + revert CannotReplaceFunctionFromNonReplacementFacet(newCurrentFacetNodeId); + } + s.facetNodes[newCurrentFacetNodeId] = + FacetNode(newFacet, oldFacetNode.prevFacetNodeId, oldFacetNode.nextFacetNodeId); + /** + * Update linked list. + */ + if (oldCurrentFacetNodeId == facetList.headFacetNodeId) { + facetList.headFacetNodeId = newCurrentFacetNodeId; + } else { + s.facetNodes[oldFacetNode.prevFacetNodeId].nextFacetNodeId = newCurrentFacetNodeId; + } + if (oldCurrentFacetNodeId == facetList.tailFacetNodeId) { + facetList.tailFacetNodeId = newCurrentFacetNodeId; + } else { + s.facetNodes[oldFacetNode.nextFacetNodeId].prevFacetNodeId = newCurrentFacetNodeId; + } + } else { + /** + * Same first selector, just replace in place. + */ + s.facetNodes[newCurrentFacetNodeId] = + FacetNode(newFacet, oldFacetNode.prevFacetNodeId, oldFacetNode.nextFacetNodeId); + /* + * If the selectors are same from both facets, then we can safely and very efficiently + * replace the old facet address with the new facet address for all the selctors. + */ + if (keccak256(oldSelectors) == keccak256(newSelectors)) { + /** + * Replace remaining selectors. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(newSelectors, selectorIndex); + s.facetNodes[selector] = FacetNode(newFacet, bytes4(0), bytes4(0)); + } + emit FacetReplaced(oldFacet, newFacet); + /* + * Restore Free Memory Pointer to reuse memory. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + continue; + } + } + + /** + * Add or replace new selectors. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(newSelectors, selectorIndex); + address existingFacet = s.facetNodes[selector].facet; + if (existingFacet == address(0)) { + unchecked { + facetList.selectorCount++; + } + } else if (existingFacet != oldFacet) { + revert CannotReplaceFunctionFromNonReplacementFacet(selector); + } + s.facetNodes[selector] = FacetNode(newFacet, bytes4(0), bytes4(0)); + } + /** + * Remove old selectors that were not replaced. + * + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors. + */ + selectorsLength = oldSelectors.length >> 2; + for (uint256 selectorIndex; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(oldSelectors, selectorIndex); + address existingFacet = s.facetNodes[selector].facet; + if (existingFacet == oldFacet) { + delete s.facetNodes[selector]; + unchecked { + facetList.selectorCount--; + } + } + } + emit FacetReplaced(oldFacet, newFacet); + /* + * Restore Free Memory Pointer to reuse memory. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + } + s.facetList = facetList; + } + + function removeFacets(address[] calldata _facets) internal { + DiamondStorage storage s = getDiamondStorage(); + FacetList memory facetList = s.facetList; + /* + * Snapshot free memory pointer. We restore this at the end of every loop + * to prevent memory expansion costs from repeated `packedSelectors` calls. + */ + uint256 freeMemPtr; + assembly ("memory-safe") { + freeMemPtr := mload(0x40) + } + for (uint256 i = 0; i < _facets.length; i++) { + address facet = _facets[i]; + bytes memory selectors = importSelectors(facet); + bytes4 currentFacetNodeId = at(selectors, 0); + FacetNode memory facetNode = s.facetNodes[currentFacetNodeId]; + /* + * This verifies that the facet we are removing exists in the + * diamond, so we can trust the rest of the selectors from `facet`. + */ + if (facetNode.facet != facet) { + revert CannotRemoveFacetThatDoesNotExist(facet); + } + /** + * Remove the facet from the linked list. + */ + if (currentFacetNodeId == facetList.headFacetNodeId) { + facetList.headFacetNodeId = facetNode.nextFacetNodeId; + } else { + s.facetNodes[facetNode.prevFacetNodeId].nextFacetNodeId = facetNode.nextFacetNodeId; + } + if (currentFacetNodeId == facetList.tailFacetNodeId) { + facetList.tailFacetNodeId = facetNode.prevFacetNodeId; + } else { + s.facetNodes[facetNode.nextFacetNodeId].prevFacetNodeId = facetNode.prevFacetNodeId; + } + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors + */ + uint256 selectorsLength = selectors.length >> 2; + /** + * Remove facet selectors. + * Because this facet is in the diamond and the `exportSelectors()` function is pure and + * immutable from the facet and we trust that, we can safely remove the selectors without + * checking that each selector belongs to the facet being removed. We know it does. + */ + for (uint256 selectorIndex; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(selectors, selectorIndex); + delete s.facetNodes[selector]; + } + unchecked { + facetList.selectorCount -= uint32(selectorsLength); + } + emit FacetRemoved(facet); + /* + * Restore Free Memory Pointer to reuse memory. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + } + facetList.facetCount -= uint32(_facets.length); + + s.facetList = facetList; + } + + /** + * @notice Upgrade the diamond by adding, replacing, or removing facets. + * + * @dev + * Facets are added first, then replaced, then removed. + * + * These events are emitted to record changes to facets: + * - `FacetAdded(address indexed _facet)` + * - `FacetReplaced(address indexed _oldFacet, address indexed _newFacet)` + * - `FacetRemoved(address indexed _facet)` + * + * If `_delegate` is non-zero, the diamond performs a `delegatecall` to + * `_delegate` using `_delegateCalldata`. The `DiamondDelegateCall` event is + * emitted. + * + * The `delegatecall` is done to alter a diamond's state or to + * initialize, modify, or remove state after an upgrade. + * + * However, if `_delegate` is zero, no `delegatecall` is made and no + * `DiamondDelegateCall` event is emitted. + * + * If _tag is non-zero or if _metadata.length > 0 then the + * `DiamondMetadata` event is emitted. + * + * @param _addFacets Facets to add. + * @param _replaceFacets (oldFacet, newFacet) pairs, to replace old with new. + * @param _removeFacets Facets to remove. + * @param _delegate Optional contract to delegatecall (zero address to skip). + * @param _delegateCalldata Optional calldata to execute on `_delegate`. + * @param _tag Optional arbitrary metadata, such as release version. + * @param _metadata Optional arbitrary data. + */ + function upgradeDiamond( + address[] calldata _addFacets, + FacetReplacement[] calldata _replaceFacets, + address[] calldata _removeFacets, + address _delegate, + bytes calldata _delegateCalldata, + bytes32 _tag, + bytes calldata _metadata + ) external { + if (getOwnerStorage().owner != msg.sender) { + revert OwnerUnauthorizedAccount(); + } + addFacets(_addFacets); + replaceFacets(_replaceFacets); + removeFacets(_removeFacets); + if (_delegate != address(0)) { + if (_delegate.code.length == 0) { + revert NoBytecodeAtAddress(_delegate); + } + (bool success, bytes memory error) = _delegate.delegatecall(_delegateCalldata); + if (!success) { + if (error.length > 0) { + /* + * bubble up error + */ + assembly ("memory-safe") { + revert(add(error, 0x20), mload(error)) + } + } else { + revert DelegateCallReverted(_delegate, _delegateCalldata); + } + } + emit DiamondDelegateCall(_delegate, _delegateCalldata); + } + if (_tag != 0 || _metadata.length > 0) { + emit DiamondMetadata(_tag, _metadata); + } + } + + /** + * @notice Exports the function selectors of the DiamondUpgradeFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the DiamondUpgradeFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.upgradeDiamond.selector); + } +} diff --git a/cli/project-template/src/templates/diamond/DiamondUpgradeMod.sol b/cli/project-template/src/templates/diamond/DiamondUpgradeMod.sol new file mode 100644 index 00000000..a1892d93 --- /dev/null +++ b/cli/project-template/src/templates/diamond/DiamondUpgradeMod.sol @@ -0,0 +1,685 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title Reference implementation for upgrade function for + * ERC-8153 Facet-Based Diamonds + * + * @dev + * Facets are stored as a doubly linked list and as a mapping of selectors to facet addresses. + * + * Facets are stored as a mapping of selectors to facet addresses for efficient delegatecall + * routing to facets. + * + * Facets are stored as a doubly linked list for efficient iteration over all facets, + * and for efficiently adding, replacing, and removing them. + * + * The `FacetList` struct contains information about the linked list of facets. + * + * Only the first FacetNode of each facet contains linked list pointers. + * * prevFacetNodeId - Is the selector of the first FacetNode of the previous + * facet. + * * nextFacetNodeId - Is the selector of the first FacetNode of the next + * facet. + * + * Here is a example that shows the structure: + * + * FacetList + * facetCount = 3 + * headFacetNodeId = selector1 // facetA + * tailFacetNodeId = selector7 // facetC + * + * facetNodes mapping (selector => FacetNode) + * + * selector facet prevFacetNodeId nextFacetNodeId + * ---------------------------------------------------------------- + * selector1 facetA 0x00000000 selector4 ← facetA LIST NODE + * selector2 facetA 0x00000000 0x00000000 + * selector3 facetA 0x00000000 0x00000000 + * + * selector4 facetB selector1 selector7 ← facetB LIST NODE + * selector5 facetB 0x00000000 0x00000000 + * selector6 facetB 0x00000000 0x00000000 + * + * selector7 facetC selector4 0x00000000 ← facetC LIST NODE + * selector8 facetC 0x00000000 0x00000000 + * selector9 facetC 0x00000000 0x00000000 + * + * Linked list order of facets: + * + * facetA (selector1) + * ↓ + * facetB (selector4) + * ↓ + * facetC (selector7) + * + * Notes: + * - Only the first selector of each facet participates in the linked list. + * - The linked list connects facets, not individual selectors. + * - Any values in "prevFacetNodeId" in non-first FacetNodes are not used. + * + * Checked/unchecked math note: + * We use unchecked math with `facetList.selectorCount` because that variable does not affect the adding, + * replacing, and removing of selectors and facets. It is used by some introspection functions. Checked math + * is used with facetList.facetCount because that affects adding, replacing, and removing selectors and facets. + * Of course these variables should never overflow/underflow anyway. + * + * Security: + * This implementation relies on the assumption that the owner of the diamond that has added or replaced any + * facet in the diamond has verified that each facet is not malicious, that each facet is immutable (not upgradeable), + * and that the `exportSelectors()` function in each facet is pure (is marked as a pure function and does not access state.) + */ + +interface IFacet { + function exportSelectors() external pure returns (bytes memory); +} + +bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("erc8153.diamond"); + +struct FacetNode { + address facet; + bytes4 prevFacetNodeId; + bytes4 nextFacetNodeId; +} + +struct FacetList { + bytes4 headFacetNodeId; + bytes4 tailFacetNodeId; + uint32 facetCount; + uint32 selectorCount; +} + +/** + * @custom:storage-location erc8042:erc8153.diamond + */ +struct DiamondStorage { + mapping(bytes4 functionSelector => FacetNode) facetNodes; + FacetList facetList; +} + +function getDiamondStorage() pure returns (DiamondStorage storage s) { + bytes32 position = DIAMOND_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Emitted when a facet is added to a diamond. + * @dev The function selectors this facet handles can be retrieved by calling + * `IFacet(_facet).exportSelectors()` + * + * @param _facet The address of the facet that handles function calls to the diamond. + */ +event FacetAdded(address indexed _facet); + +/** + * @notice Emitted when an existing facet is replaced with a new facet. + * @dev + * - Selectors that are present in the new facet but not in the old facet are added to the diamond. + * - Selectors that are present in both the new and old facet are updated to use the new facet. + * - Selectors that are not present in the new facet but are present in the old facet are removed from + * the diamond. + * + * The function selectors handled by these facets can be retrieved by calling: + * - `IFacet(_oldFacet).exportSelectors()` + * - `IFacet(_newFacet).exportSelectors()` + * + * @param _oldFacet The address of the facet that previously handled function calls to the diamond. + * @param _newFacet The address of the facet that now handles function calls to the diamond. + */ +event FacetReplaced(address indexed _oldFacet, address indexed _newFacet); + +/** + * @notice Emitted when a facet is removed from a diamond. + * @dev The function selectors this facet handles can be retrieved by calling + * `IFacet(_facet).exportSelectors()` + * + * @param _facet The address of the facet that previously handled function calls to the diamond. + */ +event FacetRemoved(address indexed _facet); + +/** + * @notice Emitted when a diamond's constructor function or function from a + * facet makes a `delegatecall`. + * + * @param _delegate The contract that was delegatecalled. + * @param _delegateCalldata The function call, including function selector and + * any arguments. + */ +event DiamondDelegateCall(address indexed _delegate, bytes _delegateCalldata); + +/** + * @notice Emitted to record information about a diamond. + * @dev This event records any arbitrary metadata. + * The format of `_tag` and `_data` are not specified by the + * standard. + * + * @param _tag Arbitrary metadata, such as a release version. + * @param _data Arbitrary metadata. + */ +event DiamondMetadata(bytes32 indexed _tag, bytes _data); + +/** + * @notice The upgradeDiamond function below detects and reverts + * with the following errors. + */ +error NoSelectorsForFacet(address _facet); +error NoBytecodeAtAddress(address _contractAddress); +error CannotAddFunctionToDiamondThatAlreadyExists(bytes4 _selector); +error CannotRemoveFacetThatDoesNotExist(address _facet); +error CannotReplaceFacetWithSameFacet(address _facet); +error FacetToReplaceDoesNotExist(address _oldFacet); +error DelegateCallReverted(address _delegate, bytes _delegateCalldata); +error ExportSelectorsCallFailed(address _facet); +error IncorrectSelectorsEncoding(address _facet); + +/** + * @dev This error means that a function to replace exists in a + * facet other than the facet that was given to be replaced. + */ +error CannotReplaceFunctionFromNonReplacementFacet(bytes4 _selector); + +function importSelectors(address _facet) view returns (bytes memory selectors) { + (bool success, bytes memory data) = _facet.staticcall(abi.encodeWithSelector(IFacet.exportSelectors.selector)); + if (success == false) { + revert ExportSelectorsCallFailed(_facet); + } + /* + * Ensure the data is large enough. + * Offset (32 bytes) + array length (32 bytes) + */ + if (data.length < 64) { + if (_facet.code.length == 0) { + revert NoBytecodeAtAddress(_facet); + } else { + revert IncorrectSelectorsEncoding(_facet); + } + } + + /* Validate ABI offset == 0x20 for a single dynamic return */ + uint256 offset; + assembly ("memory-safe") { + offset := mload(add(data, 0x20)) + } + if (offset != 0x20) { + revert IncorrectSelectorsEncoding(_facet); + } + /* + * ZERO-COPY DECODE + * Instead of abi.decode(wrapper, (bytes)), which copies memory, + * we use assembly to point 'selectors' to the bytes array inside 'data'. + * The length of `data` is stored at 0 and an ABI offset is located at 0x20 (32). + * We skip over those to point `selectors` to the length of the + * bytes array. + */ + assembly ("memory-safe") { + selectors := add(data, 0x40) + } + uint256 selectorsLength = selectors.length; + unchecked { + if (selectorsLength > data.length - 64) { + revert IncorrectSelectorsEncoding(_facet); + } + } + if (selectorsLength < 4) { + revert NoSelectorsForFacet(_facet); + } + /* + * Function selectors are strictly 4 bytes. We ensure the length is a multiple of 4. + */ + if (selectorsLength % 4 != 0) { + revert IncorrectSelectorsEncoding(_facet); + } + return selectors; +} + +function at(bytes memory selectors, uint256 index) pure returns (bytes4 selector) { + assembly ("memory-safe") { + /** + * 1. Calculate Pointer + * add(selectors, 32) - skips the length field of the bytes array + * shl(2, index) is the same as index * 4 but cheaper + * This line executes: ptr = selectorsLength + (4 * index) + */ + let ptr := add(add(selectors, 32), shl(2, index)) + /** + * 2. Load & Return + * We load 32 bytes, but Solidity truncates to 4 bytes automatically + * upon return of this function, so masking is unnecessary. + */ + selector := mload(ptr) + } +} + +function addFacets(address[] calldata _facets) { + DiamondStorage storage s = getDiamondStorage(); + uint256 facetLength = _facets.length; + if (facetLength == 0) { + return; + } + FacetList memory facetList = s.facetList; + /* + * Snapshot free memory pointer. We restore this at the end of every loop + * to prevent memory expansion costs from repeated `packedSelectors` calls. + */ + uint256 freeMemPtr; + assembly ("memory-safe") { + freeMemPtr := mload(0x40) + } + /* Algorithm Description: + * The first facet is handled separately to initialize the linked list pointers in the FacetNodes. + * This allows us to avoid additional conditional checks for linked list management in the main facet loop. + * + * For the first facet, we link the first selector to the previous facet or if this is the first facet in + * the diamond then we assign the first selector to facetList.firstFacetNodeId. + * + * All the selectors (except the first one) in the first facet are then added to the diamond. + * + * In the first iteration of the main facet loop the the selectors for the next facet are retrieved. + * This makes available the nextFacetNodeId value that is needed to store the first selector of the + * first facet. So then the first selector is stored. + * + * Then the selectors which were already retrieved for the next facet are stored, except the first selector. + * Then in the next iteration the selectors of the next facet are retrieved. This makes available the nextFacetNodeId + * value that is needed to store the first selector of the previous facet. The first selector is then stored. The loop + * continues. + * + * After the main facet loop ends, the first selector from the last facet is added to the diamond. + */ + + bytes4 prevFacetNodeId = facetList.tailFacetNodeId; + address facet = _facets[0]; + bytes memory selectors = importSelectors(facet); + /* + * currentFacetNodeId is the head node of the current facet. + * We cannot write it to storage yet because we don't know the `next` pointer. + */ + bytes4 currentFacetNodeId = at(selectors, 0); + if (facetList.facetCount == 0) { + facetList.headFacetNodeId = currentFacetNodeId; + } else { + /* + * Link the previous tail of the diamond to this new batch + */ + s.facetNodes[prevFacetNodeId].nextFacetNodeId = currentFacetNodeId; + } + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors + */ + uint256 selectorsLength = selectors.length >> 2; + unchecked { + facetList.selectorCount += uint32(selectorsLength); + } + /* + * Add all selectors, except the first, to the diamond. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(selectors, selectorIndex); + if (s.facetNodes[selector].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(selector); + } + s.facetNodes[selector] = FacetNode(facet, bytes4(0), bytes4(0)); + } + /* + * Reset memory for the main loop. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + /* + * Main facet loop. + * 1. Gets the next facet's selectors. + * 2. Now that the nextFacetNodeId value for the previous facet is available, adds the previous + * facet's first selector to the diamond. + * 3. Emits FacetAdded event for the previous facet. + * 3. Updates facet values: facet = nextFacet, etc. + * 4. Adds all the selectors (except the first) to the diamond. + * 5. Repeat loop. + */ + for (uint256 i = 1; i < facetLength; i++) { + address nextFacet = _facets[i]; + selectors = importSelectors(nextFacet); + /* + * Check to see if the PENDING first selector (from previous iteration) already exists in the diamond. + */ + if (s.facetNodes[currentFacetNodeId].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(currentFacetNodeId); + } + /* + * Identify the link to the next facet + */ + bytes4 nextFacetNodeId = at(selectors, 0); + /* + * Store the previous facet's first selector. + */ + s.facetNodes[currentFacetNodeId] = FacetNode(facet, prevFacetNodeId, nextFacetNodeId); + emit FacetAdded(facet); + /* + * Move pointers forward. + * These assignments switch us from processing the previous facet's first selector to + * processing the next facet's selectors. + * `currentFacetNodeId` becomes the new pending first selector. + */ + facet = nextFacet; + prevFacetNodeId = currentFacetNodeId; + currentFacetNodeId = nextFacetNodeId; + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors. + */ + selectorsLength = selectors.length >> 2; + /* + * Add all the selectors of the facet to the diamond, except the first selector. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(selectors, selectorIndex); + if (s.facetNodes[selector].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(selector); + } + s.facetNodes[selector] = FacetNode(facet, bytes4(0), bytes4(0)); + } + unchecked { + facetList.selectorCount += uint32(selectorsLength); + } + /* + * Restore Free Memory Pointer to reuse memory from packedSelectors() calls. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + } + /* + * Validates and adds the first selector of the last facet to the diamond. + */ + if (s.facetNodes[currentFacetNodeId].facet != address(0)) { + revert CannotAddFunctionToDiamondThatAlreadyExists(currentFacetNodeId); + } + s.facetNodes[currentFacetNodeId] = FacetNode(facet, prevFacetNodeId, bytes4(0)); + emit FacetAdded(facet); + facetList.facetCount += uint32(facetLength); + + facetList.tailFacetNodeId = currentFacetNodeId; + s.facetList = facetList; +} + +/** + * @notice This struct is used to replace old facets with new facets. + */ +struct FacetReplacement { + address oldFacet; + address newFacet; +} + +function replaceFacets(FacetReplacement[] calldata _replaceFacets) { + DiamondStorage storage s = getDiamondStorage(); + FacetList memory facetList = s.facetList; + /* + * Snapshot free memory pointer. We restore this within the loop to prevent + * memory expansion costs from repeated `packedSelectors` calls. + */ + uint256 freeMemPtr; + assembly ("memory-safe") { + freeMemPtr := mload(0x40) + } + for (uint256 i; i < _replaceFacets.length; i++) { + address oldFacet = _replaceFacets[i].oldFacet; + address newFacet = _replaceFacets[i].newFacet; + if (oldFacet == newFacet) { + revert CannotReplaceFacetWithSameFacet(oldFacet); + } + bytes memory oldSelectors = importSelectors(oldFacet); + bytes memory newSelectors = importSelectors(newFacet); + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors + */ + uint256 selectorsLength = newSelectors.length >> 2; + bytes4 oldCurrentFacetNodeId = at(oldSelectors, 0); + bytes4 newCurrentFacetNodeId = at(newSelectors, 0); + + /** + * Validate old facet exists. + */ + FacetNode memory oldFacetNode = s.facetNodes[oldCurrentFacetNodeId]; + if (oldFacetNode.facet != oldFacet) { + revert FacetToReplaceDoesNotExist(oldFacet); + } + if (oldCurrentFacetNodeId != newCurrentFacetNodeId) { + /** + * Write first selector with linking info, then process remaining. + */ + address existingFacet = s.facetNodes[newCurrentFacetNodeId].facet; + if (existingFacet == address(0)) { + unchecked { + facetList.selectorCount++; + } + } else if (existingFacet != oldFacet) { + revert CannotReplaceFunctionFromNonReplacementFacet(newCurrentFacetNodeId); + } + s.facetNodes[newCurrentFacetNodeId] = + FacetNode(newFacet, oldFacetNode.prevFacetNodeId, oldFacetNode.nextFacetNodeId); + /** + * Update linked list. + */ + if (oldCurrentFacetNodeId == facetList.headFacetNodeId) { + facetList.headFacetNodeId = newCurrentFacetNodeId; + } else { + s.facetNodes[oldFacetNode.prevFacetNodeId].nextFacetNodeId = newCurrentFacetNodeId; + } + if (oldCurrentFacetNodeId == facetList.tailFacetNodeId) { + facetList.tailFacetNodeId = newCurrentFacetNodeId; + } else { + s.facetNodes[oldFacetNode.nextFacetNodeId].prevFacetNodeId = newCurrentFacetNodeId; + } + } else { + /** + * Same first selector, just replace in place. + */ + s.facetNodes[newCurrentFacetNodeId] = + FacetNode(newFacet, oldFacetNode.prevFacetNodeId, oldFacetNode.nextFacetNodeId); + /* + * If the selectors are same from both facets, then we can safely and very efficiently + * replace the old facet address with the new facet address for all the selctors. + */ + if (keccak256(oldSelectors) == keccak256(newSelectors)) { + /** + * Replace remaining selectors. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(newSelectors, selectorIndex); + s.facetNodes[selector] = FacetNode(newFacet, bytes4(0), bytes4(0)); + } + emit FacetReplaced(oldFacet, newFacet); + /* + * Restore Free Memory Pointer to reuse memory. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + continue; + } + } + + /** + * Add or replace new selectors. + */ + for (uint256 selectorIndex = 1; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(newSelectors, selectorIndex); + address existingFacet = s.facetNodes[selector].facet; + if (existingFacet == address(0)) { + unchecked { + facetList.selectorCount++; + } + } else if (existingFacet != oldFacet) { + revert CannotReplaceFunctionFromNonReplacementFacet(selector); + } + s.facetNodes[selector] = FacetNode(newFacet, bytes4(0), bytes4(0)); + } + /** + * Remove old selectors that were not replaced. + * + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors. + */ + selectorsLength = oldSelectors.length >> 2; + for (uint256 selectorIndex; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(oldSelectors, selectorIndex); + address existingFacet = s.facetNodes[selector].facet; + if (existingFacet == oldFacet) { + delete s.facetNodes[selector]; + unchecked { + facetList.selectorCount--; + } + } + } + emit FacetReplaced(oldFacet, newFacet); + /* + * Restore Free Memory Pointer to reuse memory. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + } + s.facetList = facetList; +} + +function removeFacets(address[] calldata _facets) { + DiamondStorage storage s = getDiamondStorage(); + FacetList memory facetList = s.facetList; + /* + * Snapshot free memory pointer. We restore this at the end of every loop + * to prevent memory expansion costs from repeated `packedSelectors` calls. + */ + uint256 freeMemPtr; + assembly ("memory-safe") { + freeMemPtr := mload(0x40) + } + for (uint256 i = 0; i < _facets.length; i++) { + address facet = _facets[i]; + bytes memory selectors = importSelectors(facet); + bytes4 currentFacetNodeId = at(selectors, 0); + FacetNode memory facetNode = s.facetNodes[currentFacetNodeId]; + /* + * This verifies that the facet we are removing exists in the + * diamond, so we can trust the rest of the selectors from `facet`. + */ + if (facetNode.facet != facet) { + revert CannotRemoveFacetThatDoesNotExist(facet); + } + /** + * Remove the facet from the linked list. + */ + if (currentFacetNodeId == facetList.headFacetNodeId) { + facetList.headFacetNodeId = facetNode.nextFacetNodeId; + } else { + s.facetNodes[facetNode.prevFacetNodeId].nextFacetNodeId = facetNode.nextFacetNodeId; + } + if (currentFacetNodeId == facetList.tailFacetNodeId) { + facetList.tailFacetNodeId = facetNode.prevFacetNodeId; + } else { + s.facetNodes[facetNode.nextFacetNodeId].prevFacetNodeId = facetNode.prevFacetNodeId; + } + /* + * Shift right by 2 is the same as dividing by 4, but cheaper. + * We do this to get the number of selectors + */ + uint256 selectorsLength = selectors.length >> 2; + /** + * Remove facet selectors. + * Because this facet is in the diamond and the `exportSelectors()` function is pure and + * immutable from the facet and we trust that, we can safely remove the selectors without + * checking that each selector belongs to the facet being removed. We know it does. + */ + for (uint256 selectorIndex; selectorIndex < selectorsLength; selectorIndex++) { + bytes4 selector = at(selectors, selectorIndex); + delete s.facetNodes[selector]; + } + unchecked { + facetList.selectorCount -= uint32(selectorsLength); + } + emit FacetRemoved(facet); + /* + * Restore Free Memory Pointer to reuse memory. + */ + assembly ("memory-safe") { + mstore(0x40, freeMemPtr) + } + } + facetList.facetCount -= uint32(_facets.length); + + s.facetList = facetList; +} + +/** + * @notice Upgrade the diamond by adding, replacing, or removing facets. + * + * @dev + * Facets are added first, then replaced, then removed. + * + * These events are emitted to record changes to facets: + * - `FacetAdded(address indexed _facet)` + * - `FacetReplaced(address indexed _oldFacet, address indexed _newFacet)` + * - `FacetRemoved(address indexed _facet)` + * + * If `_delegate` is non-zero, the diamond performs a `delegatecall` to + * `_delegate` using `_delegateCalldata`. The `DiamondDelegateCall` event is + * emitted. + * + * The `delegatecall` is done to alter a diamond's state or to + * initialize, modify, or remove state after an upgrade. + * + * However, if `_delegate` is zero, no `delegatecall` is made and no + * `DiamondDelegateCall` event is emitted. + * + * If _tag is non-zero or if _metadata.length > 0 then the + * `DiamondMetadata` event is emitted. + * + * @param _addFacets Facets to add. + * @param _replaceFacets (oldFacet, newFacet) pairs, to replace old with new. + * @param _removeFacets Facets to remove. + * @param _delegate Optional contract to delegatecall (zero address to skip). + * @param _delegateCalldata Optional calldata to execute on `_delegate`. + * @param _tag Optional arbitrary metadata, such as release version. + * @param _metadata Optional arbitrary data. + */ +function upgradeDiamond( + address[] calldata _addFacets, + FacetReplacement[] calldata _replaceFacets, + address[] calldata _removeFacets, + address _delegate, + bytes calldata _delegateCalldata, + bytes32 _tag, + bytes calldata _metadata +) { + addFacets(_addFacets); + replaceFacets(_replaceFacets); + removeFacets(_removeFacets); + if (_delegate != address(0)) { + if (_delegate.code.length == 0) { + revert NoBytecodeAtAddress(_delegate); + } + (bool success, bytes memory error) = _delegate.delegatecall(_delegateCalldata); + if (!success) { + if (error.length > 0) { + /* + * bubble up error + */ + assembly ("memory-safe") { + revert(add(error, 0x20), mload(error)) + } + } else { + revert DelegateCallReverted(_delegate, _delegateCalldata); + } + } + emit DiamondDelegateCall(_delegate, _delegateCalldata); + } + if (_tag != 0 || _metadata.length > 0) { + emit DiamondMetadata(_tag, _metadata); + } +} diff --git a/cli/project-template/src/templates/diamond/example/ExampleDiamond.sol b/cli/project-template/src/templates/diamond/example/ExampleDiamond.sol new file mode 100644 index 00000000..44a08a72 --- /dev/null +++ b/cli/project-template/src/templates/diamond/example/ExampleDiamond.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import "../DiamondMod.sol" as DiamondMod; +import "../../access/Owner/Data/OwnerDataMod.sol" as OwnerDataMod; +import "../../token/ERC721/Metadata/ERC721MetadataMod.sol" as ERC721MetadataMod; +import "../../interfaceDetection/ERC165/ERC165Mod.sol" as ERC165Mod; +import {IERC721} from "../../interfaces/IERC721.sol"; +import {IERC721Metadata} from "../../interfaces/IERC721Metadata.sol"; + +contract ExampleDiamond { + /** + * @notice Initializes the diamond contract with facets, owner and other data. + * @dev Adds all provided facets to the diamond's function selector mapping and sets the contract owner. + * Each facet in the array will have its function selectors registered to enable delegatecall routing. + * @param _facets Array of facet addresses and their corresponding function selectors to add to the diamond. + * @param _diamondOwner Address that will be set as the owner of the diamond contract. + */ + constructor(address[] memory _facets, address _diamondOwner) { + DiamondMod.addFacets(_facets); + + /************************************* + * Initialize storage variables + ************************************/ + + /** + * Setting the contract owner + */ + OwnerDataMod.setContractOwner(_diamondOwner); + /** + * Setting ERC721 token details + */ + ERC721MetadataMod.setMetadata({ + _name: "ExampleDiamondNFT", _symbol: "EDN", _baseURI: "https://example.com/metadata/" + }); + /** + * Registering ERC165 interfaces + */ + ERC165Mod.registerInterface(type(IERC721).interfaceId); + ERC165Mod.registerInterface(type(IERC721Metadata).interfaceId); + } + + fallback() external payable { + DiamondMod.diamondFallback(); + } + + receive() external payable {} +} diff --git a/cli/project-template/src/templates/examples/ERC20IdentifierCollisionError.sol b/cli/project-template/src/templates/examples/ERC20IdentifierCollisionError.sol new file mode 100644 index 00000000..a15d67ef --- /dev/null +++ b/cli/project-template/src/templates/examples/ERC20IdentifierCollisionError.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC20IdentifierCollisionError { + /** + * @dev Reuses the ERC20 diamond storage identifier with an incompatible layout. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc20"); + + /** + * @dev Intentionally collides with the required ERC20 storage layout. + * @custom:storage-location erc8042:erc20 + */ + struct ERC20Storage { + bool paused; + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + } + + /** + * @notice Returns the intentionally incompatible ERC20 storage struct. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Unique probe function used to keep selector validation clean. + * @return True when the incompatible layout's paused flag is set. + */ + function identifierCollisionProbe() external view returns (bool) { + return getStorage().paused; + } + + /** + * @notice Exports a non-colliding selector so identifier validation can surface. + * @return selectors The exported function selectors. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.identifierCollisionProbe.selector); + } +} diff --git a/cli/project-template/src/templates/examples/ERC20MissingExportSelector.sol b/cli/project-template/src/templates/examples/ERC20MissingExportSelector.sol new file mode 100644 index 00000000..bb7cf7f5 --- /dev/null +++ b/cli/project-template/src/templates/examples/ERC20MissingExportSelector.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC20MissingExportSelector { + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc20.metadata"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20.metadata + */ + struct ERC20MetadataStorage { + string name; + string symbol; + uint8 decimals; + } + + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the name of the token. + * @return The token name. + */ + function name() external view returns (string memory) { + return getStorage().name; + } + + /** + * @notice Returns the symbol of the token. + * @return The token symbol. + */ + function symbol() external view returns (string memory) { + return getStorage().symbol; + } + + /** + * @notice Returns the number of decimals used for token precision. + * @return The number of decimals. + */ + function decimals() external view returns (uint8) { + return getStorage().decimals; + } + + /** + * @notice Exports an intentionally incomplete selector list for validation testing. + * @dev Missing this.decimals.selector on purpose. + * @return selectors The exported function selectors. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.name.selector, this.symbol.selector); + } +} diff --git a/cli/project-template/src/templates/examples/ERC20SelectorCollisionError.sol b/cli/project-template/src/templates/examples/ERC20SelectorCollisionError.sol new file mode 100644 index 00000000..ca401f2e --- /dev/null +++ b/cli/project-template/src/templates/examples/ERC20SelectorCollisionError.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC20SelectorCollisionError { + /** + * @notice Thrown when an account has insufficient balance for a transfer or burn. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + */ + error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + + /** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender Invalid sender address. + */ + error ERC20InvalidSender(address _sender); + + /** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver Invalid receiver address. + */ + error ERC20InvalidReceiver(address _receiver); + + /** + * @notice Thrown when a spender tries to use more than the approved allowance. + * @param _spender Address attempting to spend. + * @param _allowance Current allowance for the spender. + * @param _needed Amount required to complete the operation. + */ + error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + + /** + * @notice Thrown when the spender address is invalid (e.g., zero address). + * @param _spender Invalid spender address. + */ + error ERC20InvalidSpender(address _spender); + + /** + * @notice Emitted when tokens are transferred between two addresses. + * @param _from Address sending the tokens. + * @param _to Address receiving the tokens. + * @param _value Amount of tokens transferred. + */ + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc20"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20 + */ + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + } + + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Transfers tokens to another address. + * @dev Emits a {Transfer} event. + * @param _to The address to receive the tokens. + * @param _value The amount of tokens to transfer. + * @return True if the transfer was successful. + */ + function transfer(address _to, uint256 _value) external returns (bool) { + ERC20Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 fromBalance = s.balanceOf[msg.sender]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(msg.sender, fromBalance, _value); + } + unchecked { + s.balanceOf[msg.sender] = fromBalance - _value; + } + s.balanceOf[_to] += _value; + emit Transfer(msg.sender, _to, _value); + return true; + } + + /** + * @notice Transfers tokens on behalf of another account, provided sufficient allowance exists. + * @dev Emits a {Transfer} event and decreases the spender's allowance. + * @param _from The address to transfer tokens from. + * @param _to The address to transfer tokens to. + * @param _value The amount of tokens to transfer. + * @return True if the transfer was successful. + */ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + ERC20Storage storage s = getStorage(); + if (_from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 currentAllowance = s.allowance[_from][msg.sender]; + if (currentAllowance < _value) { + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + } + uint256 fromBalance = s.balanceOf[_from]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(_from, fromBalance, _value); + } + unchecked { + if (currentAllowance != type(uint256).max) { + s.allowance[_from][msg.sender] = currentAllowance - _value; + } + s.balanceOf[_from] = fromBalance - _value; + } + s.balanceOf[_to] += _value; + emit Transfer(_from, _to, _value); + return true; + } + + /** + * @notice Exports selectors that intentionally collide with ERC20TransferFacet. + * @dev This local example is used to test selector collision validation. + * @return selectors The exported function selectors. + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.transfer.selector, this.transferFrom.selector); + } +} diff --git a/cli/project-template/src/templates/interfaceDetection/ERC165/ERC165Facet.sol b/cli/project-template/src/templates/interfaceDetection/ERC165/ERC165Facet.sol new file mode 100644 index 00000000..454e17e9 --- /dev/null +++ b/cli/project-template/src/templates/interfaceDetection/ERC165/ERC165Facet.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-165 Standard Interface Detection Interface + * @notice Interface for detecting what interfaces a contract implements + * @dev ERC-165 allows contracts to publish their supported interfaces + */ +interface IERC165 { + /** + * @notice Query if a contract implements an interface + * @param _interfaceId The interface identifier, as specified in ERC-165 + * @dev Interface identification is specified in ERC-165. This function + * uses less than 30,000 gas. + * @return `true` if the contract implements `_interfaceId` and + * `_interfaceId` is not 0xffffffff, `false` otherwise + */ + function supportsInterface(bytes4 _interfaceId) external view returns (bool); +} + +/** + * @title ERC165Facet — ERC-165 Standard Interface Detection Facet + * @notice Facet implementation of ERC-165 for diamond proxy pattern + * @dev Allows querying which interfaces are implemented by the diamond + * Each facet is a standalone source code file following SCOP principles. + */ +contract ERC165Facet { + /** + * @notice Storage slot identifier for ERC-165 interface detection + * @dev Defined using keccak256 hash following ERC-8042 standard + */ + bytes32 constant STORAGE_POSITION = keccak256("erc165"); + + /** + * @notice ERC-165 storage layout using the ERC-8042 standard + * @custom:storage-location erc8042:erc165 + */ + struct ERC165Storage { + /** + * @notice Mapping of interface IDs to whether they are supported + */ + mapping(bytes4 => bool) supportedInterfaces; + } + + /** + * @notice Returns a pointer to the ERC-165 storage struct + * @dev Uses inline assembly to bind the storage struct to the fixed storage position + * @return s The ERC-165 storage struct + */ + function getStorage() internal pure returns (ERC165Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Query if a contract implements an interface + * @param _interfaceId The interface identifier, as specified in ERC-165 + * @dev This function checks if the diamond supports the given interface ID + * @return `true` if the contract implements `_interfaceId` and + * `_interfaceId` is not 0xffffffff, `false` otherwise + */ + function supportsInterface(bytes4 _interfaceId) external view returns (bool) { + ERC165Storage storage s = getStorage(); + + /** + * If the ERC165 interface itself is being queried, return true + * since this facet implements ERC165 + */ + if (_interfaceId == type(IERC165).interfaceId) { + return true; + } + + return s.supportedInterfaces[_interfaceId]; + } + + /** + * @notice Exports the function selectors of the ERC165Facet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC165Facet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.supportsInterface.selector); + } +} diff --git a/cli/project-template/src/templates/interfaceDetection/ERC165/ERC165Mod.sol b/cli/project-template/src/templates/interfaceDetection/ERC165/ERC165Mod.sol new file mode 100644 index 00000000..37f86375 --- /dev/null +++ b/cli/project-template/src/templates/interfaceDetection/ERC165/ERC165Mod.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title LibERC165 — ERC-165 Standard Interface Detection Library + * @notice Provides internal functions and storage layout for ERC-165 interface detection. + * @dev Uses ERC-8042 for storage location standardization + */ + +/* + * Storage slot identifier, defined using keccak256 hash of the library diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc165"); + +/* + * @notice ERC-165 storage layout using the ERC-8042 standard. + * @custom:storage-location erc8042:erc165 + */ +struct ERC165Storage { + /* + * @notice Mapping of interface IDs to whether they are supported + */ + mapping(bytes4 => bool) supportedInterfaces; +} + +/** + * @notice Returns a pointer to the ERC-165 storage struct. + * @dev Uses inline assembly to bind the storage struct to the fixed storage position. + * @return s The ERC-165 storage struct. + */ +function getStorage() pure returns (ERC165Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Register that a contract supports an interface + * @param _interfaceId The interface ID to register + * @dev Call this function during initialization to register supported interfaces. + * For example, in an ERC721 facet initialization, you would call: + * `LibERC165.registerInterface(type(IERC721).interfaceId)` + */ +function registerInterface(bytes4 _interfaceId) { + ERC165Storage storage s = getStorage(); + s.supportedInterfaces[_interfaceId] = true; +} diff --git a/cli/project-template/src/templates/interfaces/IERC1155.sol b/cli/project-template/src/templates/interfaces/IERC1155.sol new file mode 100644 index 00000000..4f544943 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC1155.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Multi Token Standard Interface + * @notice Interface for ERC-1155 token contracts with custom errors + * @dev This interface includes all custom errors used by ERC-1155 implementations (ERC-6093) + */ +interface IERC1155 { + /** + * @notice Error indicating insufficient balance for a transfer. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + * @param _tokenId The token ID involved. + */ + error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + + /** + * @notice Error indicating the sender address is invalid. + * @param _sender Invalid sender address. + */ + error ERC1155InvalidSender(address _sender); + + /** + * @notice Error indicating the receiver address is invalid. + * @param _receiver Invalid receiver address. + */ + error ERC1155InvalidReceiver(address _receiver); + + /** + * @notice Error indicating missing approval for an operator. + * @param _operator Address attempting the operation. + * @param _owner The token owner. + */ + error ERC1155MissingApprovalForAll(address _operator, address _owner); + + /** + * @notice Error indicating the approver address is invalid. + * @param _approver Invalid approver address. + */ + error ERC1155InvalidApprover(address _approver); + + /** + * @notice Error indicating the operator address is invalid. + * @param _operator Invalid operator address. + */ + error ERC1155InvalidOperator(address _operator); + + /** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ + error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + /** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ + + event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value + ); + + /** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ + event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values + ); + + /** + * @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens. + * @param _account The token owner granting/revoking approval. + * @param _operator The address being approved/revoked. + * @param _approved True if approval is granted, false if revoked. + */ + event ApprovalForAll(address indexed _account, address indexed _operator, bool _approved); + + /** + * @notice Emitted when the URI for token type `id` changes to `value`. + * @param _value The new URI for the token type. + * @param _id The token type whose URI changed. + */ + event URI(string _value, uint256 indexed _id); + + /** + * @notice Returns the amount of tokens of token type `id` owned by `account`. + * @param _account The address to query the balance of. + * @param _id The token type to query. + * @return The balance of the token type. + */ + function balanceOf(address _account, uint256 _id) external view returns (uint256); + + /** + * @notice Batched version of {balanceOf}. + * @param _accounts The addresses to query the balances of (order and length must match _ids array). + * @param _ids The token types to query (order and length must match _accounts array). + * @return The balances of the token types. + */ + function balanceOfBatch(address[] calldata _accounts, uint256[] calldata _ids) + external + view + returns (uint256[] memory); + + /** + * @notice Grants or revokes permission to `operator` to transfer the caller's tokens. + * @param _operator The address to grant/revoke approval to. + * @param _approved True to approve, false to revoke. + */ + function setApprovalForAll(address _operator, bool _approved) external; + + /** + * @notice Returns true if `operator` is approved to transfer `account`'s tokens. + * @param _account The token owner. + * @param _operator The operator to query. + * @return True if the operator is approved, false otherwise. + */ + function isApprovedForAll(address _account, address _operator) external view returns (bool); + + /** + * @notice Transfers `value` amount of token type `id` from `from` to `to`. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _id The token type to transfer. + * @param _value The amount to transfer. + * @param _data Additional data with no specified format. + */ + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external; + + /** + * @notice Batched version of {safeTransferFrom}. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _ids The token types to transfer (order and length must match _values array). + * @param _values The amounts to transfer (order and length must match _ids array). + * @param _data Additional data with no specified format. + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external; + + /** + * @notice Returns the URI for token type `id`. + * @param _id The token type to query. + * @return The URI for the token type. + */ + function uri(uint256 _id) external view returns (string memory); +} diff --git a/cli/project-template/src/templates/interfaces/IERC1155Receiver.sol b/cli/project-template/src/templates/interfaces/IERC1155Receiver.sol new file mode 100644 index 00000000..196668a9 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC1155Receiver.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Token Receiver Interface + * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. + */ +interface IERC1155Receiver { + /** + * @notice Handles the receipt of a single ERC-1155 token type. + * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. + * + * IMPORTANT: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param _operator The address which initiated the transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _id The ID of the token being transferred. + * @param _value The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed. + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) + external + returns (bytes4); + + /** + * @notice Handles the receipt of multiple ERC-1155 token types. + * @dev This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. + * + * IMPORTANT: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param _operator The address which initiated the batch transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _ids An array containing ids of each token being transferred (order and length must match _values array). + * @param _values An array containing amounts of each token being transferred (order and length must match _ids array). + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed. + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} diff --git a/cli/project-template/src/templates/interfaces/IERC173.sol b/cli/project-template/src/templates/interfaces/IERC173.sol new file mode 100644 index 00000000..4f5c2a63 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC173.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-173 Contract Ownership Standard Interface + * @notice Interface for contract ownership with custom errors + * @dev This interface includes all custom errors used by ERC-173 implementations + */ +interface IERC173 { + /** + * @notice Thrown when attempting to transfer ownership while not being the owner. + */ + error OwnableUnauthorizedAccount(); + + /** + * @notice Thrown when attempting to transfer ownership of a renounced contract. + */ + error OwnableAlreadyRenounced(); + + /** + * @dev This emits when ownership of a contract changes. + */ + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @notice Get the address of the owner + * @return The address of the owner. + */ + function owner() external view returns (address); + + /** + * @notice Set the address of the new owner of the contract + * @dev Set _newOwner to address(0) to renounce any ownership. + * @param _newOwner The address of the new owner of the contract + */ + function transferOwnership(address _newOwner) external; +} diff --git a/cli/project-template/src/templates/interfaces/IERC20.sol b/cli/project-template/src/templates/interfaces/IERC20.sol new file mode 100644 index 00000000..d1936dd9 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC20.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-20 Token Standard Interface + * @notice Interface for ERC-20 token contracts with custom errors + * @dev This interface includes all custom errors used by ERC-20 implementations + */ +interface IERC20 { + /** + * @notice Thrown when an account has insufficient balance for a transfer or burn. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + */ + error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + + /** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender Invalid sender address. + */ + error ERC20InvalidSender(address _sender); + + /** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver Invalid receiver address. + */ + error ERC20InvalidReceiver(address _receiver); + + /** + * @notice Thrown when a spender tries to use more than the approved allowance. + * @param _spender Address attempting to spend. + * @param _allowance Current allowance for the spender. + * @param _needed Amount required to complete the operation. + */ + error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + + /** + * @notice Thrown when the spender address is invalid (e.g., zero address). + * @param _spender Invalid spender address. + */ + error ERC20InvalidSpender(address _spender); + + /** + * @notice Thrown when a permit signature is invalid or expired. + * @param _owner The address that signed the permit. + * @param _spender The address that was approved. + * @param _value The amount that was approved. + * @param _deadline The deadline for the permit. + * @param _v The recovery byte of the signature. + * @param _r The r value of the signature. + * @param _s The s value of the signature. + */ + error ERC2612InvalidSignature( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s + ); + + /** + * @notice Emitted when tokens are transferred between two addresses. + * @param _from Address sending the tokens. + * @param _to Address receiving the tokens. + * @param _value Amount of tokens transferred. + */ + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + /** + * @notice Emitted when an approval is made for a spender by an owner. + * @param _owner The address granting the allowance. + * @param _spender The address receiving the allowance. + * @param _value The amount approved. + */ + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + /** + * @notice Returns the name of the token. + * @return The token name. + */ + function name() external view returns (string memory); + + /** + * @notice Returns the symbol of the token. + * @return The token symbol. + */ + function symbol() external view returns (string memory); + + /** + * @notice Returns the number of decimals used for token precision. + * @return The number of decimals. + */ + function decimals() external view returns (uint8); + + /** + * @notice Returns the total supply of tokens. + * @return The total token supply. + */ + function totalSupply() external view returns (uint256); + + /** + * @notice Returns the balance of a specific account. + * @param _account The address of the account. + * @return The account balance. + */ + function balanceOf(address _account) external view returns (uint256); + + /** + * @notice Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner. + * @param _owner The address of the token owner. + * @param _spender The address of the spender. + * @return The remaining allowance. + */ + function allowance(address _owner, address _spender) external view returns (uint256); + + /** + * @notice Approves a spender to transfer up to a certain amount of tokens on behalf of the caller. + * @dev Emits an {Approval} event. + * @param _spender The address approved to spend tokens. + * @param _value The number of tokens to approve. + * @return True if the operation succeeded. + */ + function approve(address _spender, uint256 _value) external returns (bool); + + /** + * @notice Transfers tokens to another address. + * @dev Emits a {Transfer} event. + * @param _to The address to receive the tokens. + * @param _value The amount of tokens to transfer. + * @return True if the operation succeeded. + */ + function transfer(address _to, uint256 _value) external returns (bool); + + /** + * @notice Transfers tokens on behalf of another account, provided sufficient allowance exists. + * @dev Emits a {Transfer} event and decreases the spender's allowance. + * @param _from The address to transfer tokens from. + * @param _to The address to transfer tokens to. + * @param _value The amount of tokens to transfer. + * @return True if the operation succeeded. + */ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool); + + /** + * @notice Burns (destroys) a specific amount of tokens from the caller's balance. + * @dev Emits a {Transfer} event to the zero address. + * @param _value The amount of tokens to burn. + */ + function burn(uint256 _value) external; + + /** + * @notice Burns tokens from another account, deducting from the caller's allowance. + * @dev Emits a {Transfer} event to the zero address. + * @param _account The address whose tokens will be burned. + * @param _value The amount of tokens to burn. + */ + function burnFrom(address _account, uint256 _value) external; + + /** + * @notice Returns the current nonce for an owner. + * @dev This value changes each time a permit is used. + * @param _owner The address of the owner. + * @return The current nonce. + */ + function nonces(address _owner) external view returns (uint256); + + /** + * @notice Returns the domain separator used in the encoding of the signature for {permit}. + * @dev This value is unique to a contract and chain ID combination to prevent replay attacks. + * @return The domain separator. + */ + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /** + * @notice Sets the allowance for a spender via a signature. + * @dev This function implements EIP-2612 permit functionality. + * @param _owner The address of the token owner. + * @param _spender The address of the spender. + * @param _value The amount of tokens to approve. + * @param _deadline The deadline for the permit (timestamp). + * @param _v The recovery byte of the signature. + * @param _r The r value of the signature. + * @param _s The s value of the signature. + */ + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external; +} diff --git a/cli/project-template/src/templates/interfaces/IERC2981.sol b/cli/project-template/src/templates/interfaces/IERC2981.sol new file mode 100644 index 00000000..6de2c31d --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC2981.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-2981 NFT Royalty Standard Interface + * @notice Interface for ERC-2981 royalty information with custom errors + * @dev This interface includes all custom errors used by ERC-2981 implementations + */ +interface IERC2981 { + /** + * @notice Error indicating the default royalty fee exceeds 100% (10000 basis points). + */ + error ERC2981InvalidDefaultRoyalty(uint256 _numerator, uint256 _denominator); + + /** + * @notice Error indicating the default royalty receiver is the zero address. + */ + error ERC2981InvalidDefaultRoyaltyReceiver(address _receiver); + + /** + * @notice Error indicating a token-specific royalty fee exceeds 100% (10000 basis points). + */ + error ERC2981InvalidTokenRoyalty(uint256 _tokenId, uint256 _numerator, uint256 _denominator); + + /** + * @notice Error indicating a token-specific royalty receiver is the zero address. + */ + error ERC2981InvalidTokenRoyaltyReceiver(uint256 _tokenId, address _receiver); + + /** + * @notice Returns royalty information for a given token and sale price. + * @dev Called with the sale price to determine how much royalty is owed and to whom. + * Implementations MUST calculate royalty as a percentage of the sale price. + * @param _tokenId The NFT asset queried for royalty information. + * @param _salePrice The sale price of the NFT asset specified by _tokenId. + * @return receiver The address designated to receive the royalty payment. + * @return royaltyAmount The royalty payment amount for _salePrice. + */ + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount); +} diff --git a/cli/project-template/src/templates/interfaces/IERC6909.sol b/cli/project-template/src/templates/interfaces/IERC6909.sol new file mode 100644 index 00000000..d4a4dc41 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC6909.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-6909 Minimal Multi-Token Interface + * @notice Interface for ERC-6909 multi-token contracts with custom errors. + */ +interface IERC6909 { + /** + * @notice Thrown when the sender has insufficient balance. + */ + error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); + + /** + * @notice Thrown when the spender has insufficient allowance. + */ + error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); + + /** + * @notice Thrown when the approver address is invalid. + */ + error ERC6909InvalidApprover(address _approver); + + /** + * @notice Thrown when the receiver address is invalid. + */ + error ERC6909InvalidReceiver(address _receiver); + + /** + * @notice Thrown when the sender address is invalid. + */ + error ERC6909InvalidSender(address _sender); + + /** + * @notice Thrown when the spender address is invalid. + */ + error ERC6909InvalidSpender(address _spender); + + /** + * @notice Emitted when a transfer occurs. + */ + event Transfer( + address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount + ); + + /** + * @notice Emitted when an operator is set. + */ + event OperatorSet(address indexed _owner, address indexed _spender, bool _approved); + + /** + * @notice Emitted when an approval occurs. + */ + event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _amount); + + /** + * @notice Owner balance of an id. + * @param _owner The address of the owner. + * @param _id The id of the token. + * @return The balance of the token. + */ + function balanceOf(address _owner, uint256 _id) external view returns (uint256); + + /** + * @notice Spender allowance of an id. + * @param _owner The address of the owner. + * @param _spender The address of the spender. + * @param _id The id of the token. + * @return The allowance of the token. + */ + function allowance(address _owner, address _spender, uint256 _id) external view returns (uint256); + + /** + * @notice Checks if a spender is approved by an owner as an operator. + * @param _owner The address of the owner. + * @param _spender The address of the spender. + * @return The approval status. + */ + function isOperator(address _owner, address _spender) external view returns (bool); + + /** + * @notice Approves an amount of an id to a spender. + * @param _spender The address of the spender. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the approval succeeded. + */ + function approve(address _spender, uint256 _id, uint256 _amount) external returns (bool); + + /** + * @notice Sets or removes a spender as an operator for the caller. + * @param _spender The address of the spender. + * @param _approved The approval status. + * @return Whether the operator update succeeded. + */ + function setOperator(address _spender, bool _approved) external returns (bool); + + /** + * @notice Transfers an amount of an id from the caller to a receiver. + * @param _receiver The address of the receiver. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the transfer succeeded. + */ + function transfer(address _receiver, uint256 _id, uint256 _amount) external returns (bool); + + /** + * @notice Transfers an amount of an id from a sender to a receiver. + * @param _sender The address of the sender. + * @param _receiver The address of the receiver. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the transfer succeeded. + */ + function transferFrom(address _sender, address _receiver, uint256 _id, uint256 _amount) external returns (bool); +} diff --git a/cli/project-template/src/templates/interfaces/IERC721.sol b/cli/project-template/src/templates/interfaces/IERC721.sol new file mode 100644 index 00000000..b30600fd --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC721.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Token Standard Interface + * @notice Interface for ERC-721 token contracts with custom errors + * @dev This interface includes all custom errors used by ERC-721 implementations + * Note: the ERC-165 identifier for this interface is 0x80ac58cd. + */ +interface IERC721 { + /** + * @notice Error indicating the queried owner address is invalid (zero address). + */ + error ERC721InvalidOwner(address _owner); + + /** + * @notice Error indicating that the queried token does not exist. + */ + error ERC721NonexistentToken(uint256 _tokenId); + + /** + * @notice Error indicating the sender does not match the token owner. + */ + error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + + /** + * @notice Error indicating the sender address is invalid. + */ + error ERC721InvalidSender(address _sender); + + /** + * @notice Error indicating the receiver address is invalid. + */ + error ERC721InvalidReceiver(address _receiver); + + /** + * @notice Error indicating the operator lacks approval to transfer the given token. + */ + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + /** + * @notice Error indicating the approver address is invalid. + */ + error ERC721InvalidApprover(address _approver); + + /** + * @notice Error indicating the operator address is invalid. + */ + error ERC721InvalidOperator(address _operator); + + /** + * @notice Emitted when ownership of an NFT changes by any mechanism. + */ + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + /** + * @notice Emitted when the approved address for an NFT is changed or reaffirmed. + */ + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + + /** + * @notice Emitted when an operator is enabled or disabled for an owner. + */ + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + /** + * @notice Returns the number of tokens owned by a given address. + * @param _owner The address to query the balance of. + * @return The balance (number of tokens) owned by `_owner`. + */ + function balanceOf(address _owner) external view returns (uint256); + + /** + * @notice Returns the owner of a given token ID. + * @param _tokenId The token ID to query. + * @return The address of the token owner. + */ + function ownerOf(uint256 _tokenId) external view returns (address); + + /** + * @notice Returns the approved address for a given token ID. + * @param _tokenId The token ID to query the approval of. + * @return The approved address for the token. + */ + function getApproved(uint256 _tokenId) external view returns (address); + + /** + * @notice Returns true if an operator is approved to manage all of an owner's assets. + * @param _owner The token owner. + * @param _operator The operator address. + * @return True if the operator is approved for all tokens of the owner. + */ + function isApprovedForAll(address _owner, address _operator) external view returns (bool); + + /** + * @notice Approves another address to transfer the given token ID. + * @param _approved The address to be approved. + * @param _tokenId The token ID to approve. + */ + function approve(address _approved, uint256 _tokenId) external; + + /** + * @notice Approves or revokes permission for an operator to manage all caller's assets. + * @param _operator The operator address to set approval for. + * @param _approved True to approve, false to revoke. + */ + function setApprovalForAll(address _operator, bool _approved) external; + + /** + * @notice Transfers a token from one address to another. + * @param _from The current owner of the token. + * @param _to The address to receive the token. + * @param _tokenId The token ID to transfer. + */ + function transferFrom(address _from, address _to, uint256 _tokenId) external; + + /** + * @notice Safely transfers a token, checking if the receiver can handle ERC-721 tokens. + * @param _from The current owner of the token. + * @param _to The address to receive the token. + * @param _tokenId The token ID to transfer. + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external; + + /** + * @notice Safely transfers a token with additional data. + * @param _from The current owner of the token. + * @param _to The address to receive the token. + * @param _tokenId The token ID to transfer. + * @param _data Additional data with no specified format. + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external; +} diff --git a/cli/project-template/src/templates/interfaces/IERC721Enumerable.sol b/cli/project-template/src/templates/interfaces/IERC721Enumerable.sol new file mode 100644 index 00000000..688ed9f4 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC721Enumerable.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Enumerable Token Standard Interface + * @notice Interface for ERC-721 token contracts with enumeration support and custom errors + * @dev This interface includes all custom errors used by ERC-721 Enumerable implementations + */ +interface IERC721Enumerable { + /** + * @notice Thrown when querying or transferring from an invalid owner address. + */ + error ERC721InvalidOwner(address _owner); + + /** + * @notice Thrown when operating on a non-existent token. + */ + error ERC721NonexistentToken(uint256 _tokenId); + + /** + * @notice Thrown when the provided owner does not match the actual owner of the token. + */ + error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + + /** + * @notice Thrown when the sender address is invalid. + */ + error ERC721InvalidSender(address _sender); + + /** + * @notice Thrown when the receiver address is invalid. + */ + error ERC721InvalidReceiver(address _receiver); + + /** + * @notice Thrown when the operator lacks sufficient approval for a transfer. + */ + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + /** + * @notice Thrown when an invalid approver is provided. + */ + error ERC721InvalidApprover(address _approver); + + /** + * @notice Thrown when an invalid operator is provided. + */ + error ERC721InvalidOperator(address _operator); + + /** + * @notice Thrown when an index is out of bounds during enumeration. + */ + error ERC721OutOfBoundsIndex(address _owner, uint256 _index); + + /** + * @notice Emitted when a token is transferred between addresses. + */ + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + /** + * @notice Emitted when a token is approved for transfer by another address. + */ + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + + /** + * @notice Emitted when an operator is approved or revoked for all tokens of an owner. + */ + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + /** + * @notice Returns the name of the token collection. + * @return The token collection name. + */ + function name() external view returns (string memory); + + /** + * @notice Returns the symbol of the token collection. + * @return The token symbol. + */ + function symbol() external view returns (string memory); + + /** + * @notice Returns the total number of tokens in existence. + * @return The total supply of tokens. + */ + function totalSupply() external view returns (uint256); + + /** + * @notice Returns the number of tokens owned by an address. + * @param _owner The address to query. + * @return The balance (number of tokens owned). + */ + function balanceOf(address _owner) external view returns (uint256); + + /** + * @notice Returns the owner of a given token ID. + * @param _tokenId The token ID to query. + * @return The address of the token owner. + */ + function ownerOf(uint256 _tokenId) external view returns (address); + + /** + * @notice Returns a token ID owned by a given address at a specific index. + * @param _owner The address to query. + * @param _index The index of the token. + * @return The token ID owned by `_owner` at `_index`. + */ + function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256); + + /** + * @notice Returns the approved address for a given token ID. + * @param _tokenId The token ID to query. + * @return The approved address for the token. + */ + function getApproved(uint256 _tokenId) external view returns (address); + + /** + * @notice Returns whether an operator is approved for all tokens of an owner. + * @param _owner The token owner. + * @param _operator The operator address. + * @return True if approved for all, false otherwise. + */ + function isApprovedForAll(address _owner, address _operator) external view returns (bool); + + /** + * @notice Approves another address to transfer a specific token ID. + * @param _approved The address being approved. + * @param _tokenId The token ID to approve. + */ + function approve(address _approved, uint256 _tokenId) external; + + /** + * @notice Approves or revokes an operator to manage all tokens of the caller. + * @param _operator The operator address. + * @param _approved True to approve, false to revoke. + */ + function setApprovalForAll(address _operator, bool _approved) external; + + /** + * @notice Transfers a token from one address to another. + * @param _from The current owner of the token. + * @param _to The recipient address. + * @param _tokenId The token ID to transfer. + */ + function transferFrom(address _from, address _to, uint256 _tokenId) external; + + /** + * @notice Safely transfers a token, checking for receiver contract compatibility. + * @param _from The current owner of the token. + * @param _to The recipient address. + * @param _tokenId The token ID to transfer. + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external; + + /** + * @notice Safely transfers a token with additional data. + * @param _from The current owner of the token. + * @param _to The recipient address. + * @param _tokenId The token ID to transfer. + * @param _data Additional data to send to the receiver. + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external; +} diff --git a/cli/project-template/src/templates/interfaces/IERC721Metadata.sol b/cli/project-template/src/templates/interfaces/IERC721Metadata.sol new file mode 100644 index 00000000..0c8c60d9 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC721Metadata.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Non-Fungible Token Standard, optional metadata extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + * Note: the ERC-165 identifier for this interface is 0x5b5e139f. + */ +interface IERC721Metadata { + /** + * @notice A descriptive name for a collection of NFTs in this contract + */ + function name() external view returns (string memory _name); + + /** + * @notice An abbreviated name for NFTs in this contract + */ + function symbol() external view returns (string memory _symbol); + + /** + * @notice A distinct Uniform Resource Identifier (URI) for a given asset. + * @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC + * 3986. The URI may point to a JSON file that conforms to the "ERC721 + * Metadata JSON Schema". + */ + function tokenURI(uint256 _tokenId) external view returns (string memory); +} diff --git a/cli/project-template/src/templates/interfaces/IERC721Receiver.sol b/cli/project-template/src/templates/interfaces/IERC721Receiver.sol new file mode 100644 index 00000000..5a9f6379 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IERC721Receiver.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Token Receiver Interface + * @notice Interface for contracts that want to handle safe transfers of ERC-721 tokens. + * @dev Contracts implementing this must return the selector to confirm token receipt. + */ +interface IERC721Receiver { + /** + * @notice Handles the receipt of an NFT. + * @param _operator The address which called `safeTransferFrom`. + * @param _from The previous owner of the token. + * @param _tokenId The NFT identifier being transferred. + * @param _data Additional data with no specified format. + * @return The selector to confirm the token transfer. + */ + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) + external + returns (bytes4); +} diff --git a/cli/project-template/src/templates/interfaces/IFacet.sol b/cli/project-template/src/templates/interfaces/IFacet.sol new file mode 100644 index 00000000..46e9ab73 --- /dev/null +++ b/cli/project-template/src/templates/interfaces/IFacet.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title IFacet + * @notice Interface for a facet contract + */ +interface IFacet { + /** + * @notice Exports the selectors that are exposed by the facet. + * @return Packed selectors that are exported by the facet. + */ + function exportSelectors() external pure returns (bytes memory); +} diff --git a/cli/project-template/src/templates/libraries/NonReentrancyMod.sol b/cli/project-template/src/templates/libraries/NonReentrancyMod.sol new file mode 100644 index 00000000..e219d35e --- /dev/null +++ b/cli/project-template/src/templates/libraries/NonReentrancyMod.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title LibNonReentrancy - Non-Reentrancy Library + * @notice Provides common non-reentrant functions for Solidity contracts. + */ + +bytes32 constant NON_REENTRANT_SLOT = keccak256("compose.nonreentrant"); + +/** + * Function selector - 0x43a0d067 + */ +error Reentrancy(); + +/** + * @dev How to use as a library in user facets + */ +/* +function someFunction() external { + LibNonReentrancy.enter(); + // functions logic here... + LibNonReentrancy.exit(); +} +*/ + +/** + * @dev How to use as a modifier in user facets + */ +/* +modifier nonReentrant { + LibNonReentrancy.enter(); + _; + LibNonReentrancy.exit(); +} +*/ + +/** + * @dev This unlocks the entry into a function + */ +function enter() { + bytes32 position = NON_REENTRANT_SLOT; + assembly ("memory-safe") { + if tload(position) { + /* + * Store the selector for "Reentrancy()" (0xab143c06) at the beginning of memory. + * We shift left by 224 bits (256 - 32) to left-align the 4-byte selector in the 32-byte slot. + */ + mstore(0x00, shl(224, 0xab143c06)) + revert(0x00, 0x04) + } + tstore(position, 1) + } +} + +/** + * @dev This locks the entry into a function + */ +function exit() { + bytes32 position = NON_REENTRANT_SLOT; + assembly { + tstore(position, 0) + } +} diff --git a/cli/project-template/src/templates/libraries/Utils.sol b/cli/project-template/src/templates/libraries/Utils.sol new file mode 100644 index 00000000..3cbdc006 --- /dev/null +++ b/cli/project-template/src/templates/libraries/Utils.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title LibUtils - Utility Library + * @notice Provides common utility functions for Solidity contracts. + * @dev Implements functions for data type conversions. + */ diff --git a/cli/project-template/src/templates/token/ERC1155/Approve/ERC1155ApproveFacet.sol b/cli/project-template/src/templates/token/ERC1155/Approve/ERC1155ApproveFacet.sol new file mode 100644 index 00000000..043d12e1 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Approve/ERC1155ApproveFacet.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Approve Facet + * @notice Provides approval functionality for ERC-1155 tokens. + */ +contract ERC1155ApproveFacet { + /** + * @notice Error indicating the operator address is invalid. + * @param _operator Invalid operator address. + */ + error ERC1155InvalidOperator(address _operator); + + /** + * @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens. + * @param _account The token owner granting/revoking approval. + * @param _operator The address being approved/revoked. + * @param _approved True if approval is granted, false if revoked. + */ + event ApprovalForAll(address indexed _account, address indexed _operator, bool _approved); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ + struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; + } + + /** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ + function getStorage() internal pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Grants or revokes permission to `operator` to transfer the caller's tokens. + * @dev Emits an {ApprovalForAll} event. + * @param _operator The address to grant/revoke approval to. + * @param _approved True to approve, false to revoke. + */ + function setApprovalForAll(address _operator, bool _approved) external { + if (_operator == address(0)) { + revert ERC1155InvalidOperator(address(0)); + } + getStorage().isApprovedForAll[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + /** + * @notice Exports the function selectors of the ERC1155ApproveFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155ApproveFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.setApprovalForAll.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC1155/Approve/ERC1155ApproveMod.sol b/cli/project-template/src/templates/token/ERC1155/Approve/ERC1155ApproveMod.sol new file mode 100644 index 00000000..4dc592ca --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Approve/ERC1155ApproveMod.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Approve Module + * @notice Provides internal approval functionality for ERC-1155 tokens. + */ + +/** + * @notice Error indicating the operator address is invalid. + * @param _operator Invalid operator address. + */ +error ERC1155InvalidOperator(address _operator); + +/** + * @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens. + * @param _account The token owner granting/revoking approval. + * @param _operator The address being approved/revoked. + * @param _approved True if approval is granted, false if revoked. + */ +event ApprovalForAll(address indexed _account, address indexed _operator, bool _approved); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ +struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; +} + +/** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ +function getStorage() pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Grants or revokes permission to `operator` to transfer the user's tokens. + * @dev Emits an {ApprovalForAll} event. + * @param _user The address of the token owner. + * @param _operator The address to grant/revoke approval to. + * @param _approved True to approve, false to revoke. + */ +function setApprovalForAll(address _user, address _operator, bool _approved) { + if (_operator == address(0)) { + revert ERC1155InvalidOperator(address(0)); + } + getStorage().isApprovedForAll[_user][_operator] = _approved; + emit ApprovalForAll(_user, _operator, _approved); +} diff --git a/cli/project-template/src/templates/token/ERC1155/Burn/ERC1155BurnFacet.sol b/cli/project-template/src/templates/token/ERC1155/Burn/ERC1155BurnFacet.sol new file mode 100644 index 00000000..29583513 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Burn/ERC1155BurnFacet.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Burn Facet + * @notice Provides burn functionality for ERC-1155 tokens. + */ +contract ERC1155BurnFacet { + /** + * @notice Error indicating insufficient balance for a burn operation. + * @param _sender Address attempting the burn. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + * @param _tokenId The token ID involved. + */ + error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + + /** + * @notice Error indicating the sender address is invalid. + * @param _sender Invalid sender address. + */ + error ERC1155InvalidSender(address _sender); + + /** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ + error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + + /** + * @notice Error indicating missing approval for an operator. + * @param _operator Address attempting the operation. + * @param _owner The token owner. + */ + error ERC1155MissingApprovalForAll(address _operator, address _owner); + + /** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ + event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value + ); + + /** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ + event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values + ); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ + struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; + } + + /** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ + function getStorage() internal pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Burns a single token type from an address. + * @dev Emits a {TransferSingle} event. + * Caller must be the owner or an approved operator. + * @param _from The address whose tokens will be burned. + * @param _id The token type to burn. + * @param _value The amount of tokens to burn. + */ + function burn(address _from, uint256 _id, uint256 _value) external { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + + ERC1155Storage storage s = getStorage(); + + if (_from != msg.sender && !s.isApprovedForAll[_from][msg.sender]) { + revert ERC1155MissingApprovalForAll(msg.sender, _from); + } + + uint256 fromBalance = s.balanceOf[_id][_from]; + + if (fromBalance < _value) { + revert ERC1155InsufficientBalance(_from, fromBalance, _value, _id); + } + + unchecked { + s.balanceOf[_id][_from] = fromBalance - _value; + } + + emit TransferSingle(msg.sender, _from, address(0), _id, _value); + } + + /** + * @notice Burns multiple token types from an address in a single transaction. + * @dev Emits a {TransferBatch} event. + * Caller must be the owner or an approved operator. + * @param _from The address whose tokens will be burned. + * @param _ids The token types to burn. + * @param _values The amounts of tokens to burn for each type. + */ + function burnBatch(address _from, uint256[] calldata _ids, uint256[] calldata _values) external { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + if (_from != msg.sender && !s.isApprovedForAll[_from][msg.sender]) { + revert ERC1155MissingApprovalForAll(msg.sender, _from); + } + + for (uint256 i = 0; i < _ids.length; i++) { + uint256 id = _ids[i]; + uint256 value = _values[i]; + uint256 fromBalance = s.balanceOf[id][_from]; + + if (fromBalance < value) { + revert ERC1155InsufficientBalance(_from, fromBalance, value, id); + } + + unchecked { + s.balanceOf[id][_from] = fromBalance - value; + } + } + + emit TransferBatch(msg.sender, _from, address(0), _ids, _values); + } + + /** + * @notice Exports the function selectors of the ERC1155BurnFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155BurnFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.burn.selector, this.burnBatch.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC1155/Burn/ERC1155BurnMod.sol b/cli/project-template/src/templates/token/ERC1155/Burn/ERC1155BurnMod.sol new file mode 100644 index 00000000..cfe42fc7 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Burn/ERC1155BurnMod.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Burn Module + * @notice Provides internal burn functionality for ERC-1155 tokens. + */ + +/** + * @notice Error indicating insufficient balance for a burn operation. + * @param _sender Address attempting the burn. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + * @param _tokenId The token ID involved. + */ +error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + +/** + * @notice Error indicating the sender address is invalid. + * @param _sender Invalid sender address. + */ +error ERC1155InvalidSender(address _sender); + +/** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ +error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + +/** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ +event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value +); + +/** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ +event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values +); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ +struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; +} + +/** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ +function getStorage() pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Burns a single token type from an address. + * @dev Decreases the balance and emits a TransferSingle event. + * Reverts if the account has insufficient balance. + * This module does not perform approval checks. Ensure proper ownership or approval validation before calling this function. + * @param _from The address whose tokens will be burned. + * @param _id The token type to burn. + * @param _value The amount of tokens to burn. + */ +function burn(address _from, uint256 _id, uint256 _value) { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + + ERC1155Storage storage s = getStorage(); + uint256 fromBalance = s.balanceOf[_id][_from]; + + if (fromBalance < _value) { + revert ERC1155InsufficientBalance(_from, fromBalance, _value, _id); + } + + unchecked { + s.balanceOf[_id][_from] = fromBalance - _value; + } + + emit TransferSingle(msg.sender, _from, address(0), _id, _value); +} + +/** + * @notice Burns multiple token types from an address in a single transaction. + * @dev Decreases balances for each token type and emits a TransferBatch event. + * Reverts if the account has insufficient balance for any token type. + * This module does not perform approval checks. Ensure proper ownership or approval validation before calling this function. + * @param _from The address whose tokens will be burned. + * @param _ids The token types to burn. + * @param _values The amounts of tokens to burn for each type. + */ +function burnBatch(address _from, uint256[] memory _ids, uint256[] memory _values) { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + for (uint256 i = 0; i < _ids.length; i++) { + uint256 id = _ids[i]; + uint256 value = _values[i]; + uint256 fromBalance = s.balanceOf[id][_from]; + + if (fromBalance < value) { + revert ERC1155InsufficientBalance(_from, fromBalance, value, id); + } + + unchecked { + s.balanceOf[id][_from] = fromBalance - value; + } + } + + emit TransferBatch(msg.sender, _from, address(0), _ids, _values); +} diff --git a/cli/project-template/src/templates/token/ERC1155/Data/ERC1155DataFacet.sol b/cli/project-template/src/templates/token/ERC1155/Data/ERC1155DataFacet.sol new file mode 100644 index 00000000..7547f2ee --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Data/ERC1155DataFacet.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Data Facet + */ +contract ERC1155DataFacet { + /** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ + error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ + struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; + } + + /** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ + function getStorage() internal pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the amount of tokens of token type `id` owned by `account`. + * @param _account The address to query the balance of. + * @param _id The token type to query. + * @return The balance of the token type. + */ + function balanceOf(address _account, uint256 _id) external view returns (uint256) { + return getStorage().balanceOf[_id][_account]; + } + + /** + * @notice Batched version of {balanceOf}. + * @param _accounts The addresses to query the balances of. + * @param _ids The token types to query. + * @return balances The balances of the token types. + */ + function balanceOfBatch(address[] calldata _accounts, uint256[] calldata _ids) + external + view + returns (uint256[] memory balances) + { + if (_accounts.length != _ids.length) { + revert ERC1155InvalidArrayLength(_ids.length, _accounts.length); + } + + ERC1155Storage storage s = getStorage(); + balances = new uint256[](_accounts.length); + + for (uint256 i = 0; i < _accounts.length; i++) { + balances[i] = s.balanceOf[_ids[i]][_accounts[i]]; + } + } + + /** + * @notice Returns true if `operator` is approved to transfer `account`'s tokens. + * @param _account The token owner. + * @param _operator The operator to query. + * @return True if the operator is approved, false otherwise. + */ + function isApprovedForAll(address _account, address _operator) external view returns (bool) { + return getStorage().isApprovedForAll[_account][_operator]; + } + + /** + * @notice Exports the function selectors of the ERC1155DataFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155DataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.balanceOf.selector, this.balanceOfBatch.selector, this.isApprovedForAll.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC1155/Metadata/ERC1155MetadataFacet.sol b/cli/project-template/src/templates/token/ERC1155/Metadata/ERC1155MetadataFacet.sol new file mode 100644 index 00000000..382919fa --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Metadata/ERC1155MetadataFacet.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Metadata Facet + * @notice Provides URI metadata functionality for ERC-1155 tokens. + */ +contract ERC1155MetadataFacet { + /** + * @notice Emitted when the URI for token type `_id` changes to `_value`. + * @param _value The new URI for the token type. + * @param _id The token type whose URI changed. + */ + event URI(string _value, uint256 indexed _id); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc1155.metadata"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 metadata. + * @custom:storage-location erc8042:erc1155.metadata + */ + struct ERC1155MetadataStorage { + string uri; + string baseURI; + mapping(uint256 tokenId => string) tokenURIs; + } + + /** + * @notice Returns the ERC-1155 metadata storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 metadata storage struct reference. + */ + function getStorage() internal pure returns (ERC1155MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the URI for token type `_id`. + * @dev If a token-specific URI is set in tokenURIs[_id], returns the concatenation of baseURI and tokenURIs[_id]. + * Note that baseURI is empty by default and must be set explicitly if concatenation is desired. + * If no token-specific URI is set, returns the default URI which applies to all token types. + * The default URI may contain the substring `{id}` which clients should replace with the actual token ID. + * @param _id The token ID to query. + * @return The URI for the token type. + */ + function uri(uint256 _id) external view returns (string memory) { + ERC1155MetadataStorage storage s = getStorage(); + string memory tokenURI = s.tokenURIs[_id]; + + return bytes(tokenURI).length > 0 ? string.concat(s.baseURI, tokenURI) : s.uri; + } + + /** + * @notice Exports the function selectors of the ERC1155MetadataFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155MetadataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.uri.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC1155/Metadata/ERC1155MetadataMod.sol b/cli/project-template/src/templates/token/ERC1155/Metadata/ERC1155MetadataMod.sol new file mode 100644 index 00000000..76997eb6 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Metadata/ERC1155MetadataMod.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Metadata Module + * @notice Provides internal metadata functionality for ERC-1155 tokens. + */ + +/** + * @notice Emitted when the URI for token type `_id` changes to `_value`. + * @param _value The new URI for the token type. + * @param _id The token type whose URI changed. + */ +event URI(string _value, uint256 indexed _id); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155.metadata"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 metadata. + * @custom:storage-location erc8042:erc1155.metadata + */ +struct ERC1155MetadataStorage { + string uri; + string baseURI; + mapping(uint256 tokenId => string) tokenURIs; +} + +/** + * @notice Returns the ERC-1155 metadata storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 metadata storage struct reference. + */ +function getStorage() pure returns (ERC1155MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Sets the token-specific URI for a given token ID. + * @dev Sets tokenURIs[_tokenId] to the provided string and emits a URI event with the full computed URI. + * The emitted URI is the concatenation of baseURI and the token-specific URI. + * @param _tokenId The token ID to set the URI for. + * @param _tokenURI The token-specific URI string to be concatenated with baseURI. + */ +function setTokenURI(uint256 _tokenId, string memory _tokenURI) { + ERC1155MetadataStorage storage s = getStorage(); + s.tokenURIs[_tokenId] = _tokenURI; + + string memory fullURI = bytes(_tokenURI).length > 0 ? string.concat(s.baseURI, _tokenURI) : s.uri; + emit URI(fullURI, _tokenId); +} + +/** + * @notice Sets the base URI prefix for token-specific URIs. + * @dev The base URI is concatenated with token-specific URIs set via setTokenURI. + * Does not affect the default URI used when no token-specific URI is set. + * @param _baseURI The base URI string to prepend to token-specific URIs. + */ +function setBaseURI(string memory _baseURI) { + ERC1155MetadataStorage storage s = getStorage(); + s.baseURI = _baseURI; +} + +/** + * @notice Sets the default URI for all token types. + * @dev This URI is used when no token-specific URI is set. + * @param _uri The default URI string. + */ +function setURI(string memory _uri) { + ERC1155MetadataStorage storage s = getStorage(); + s.uri = _uri; +} diff --git a/cli/project-template/src/templates/token/ERC1155/Mint/ERC1155MintMod.sol b/cli/project-template/src/templates/token/ERC1155/Mint/ERC1155MintMod.sol new file mode 100644 index 00000000..2392536a --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Mint/ERC1155MintMod.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Token Receiver Interface + * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. + */ +interface IERC1155Receiver { + /** + * @notice Handles the receipt of a single ERC-1155 token type. + * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. + * + * IMPORTANT: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param _operator The address which initiated the transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _id The ID of the token being transferred. + * @param _value The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed. + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) + external + returns (bytes4); + + /** + * @notice Handles the receipt of multiple ERC-1155 token types. + * @dev This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. + * + * IMPORTANT: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param _operator The address which initiated the batch transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _ids An array containing ids of each token being transferred (order and length must match _values array). + * @param _values An array containing amounts of each token being transferred (order and length must match _ids array). + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed. + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} + +/** + * @title ERC-1155 Mint Module + * @notice Provides internal mint functionality for ERC-1155 tokens. + */ + +/** + * @notice Error indicating the receiver address is invalid. + * @param _receiver Invalid receiver address. + */ +error ERC1155InvalidReceiver(address _receiver); + +/** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ +error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + +/** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ +event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value +); + +/** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ +event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values +); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ +struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; +} + +/** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ +function getStorage() pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Mints a single token type to an address. + * @dev Increases the balance and emits a TransferSingle event. + * Performs receiver validation if recipient is a contract. + * @param _to The address that will receive the tokens. + * @param _id The token type to mint. + * @param _value The amount of tokens to mint. + * @param _data Additional data with no specified format. + */ +function mint(address _to, uint256 _id, uint256 _value, bytes memory _data) { + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + + ERC1155Storage storage s = getStorage(); + s.balanceOf[_id][_to] += _value; + + emit TransferSingle(msg.sender, address(0), _to, _id, _value); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155Received(msg.sender, address(0), _id, _value, _data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert ERC1155InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } +} + +/** + * @notice Mints multiple token types to an address in a single transaction. + * @dev Increases balances for each token type and emits a TransferBatch event. + * Performs receiver validation if recipient is a contract. + * @param _to The address that will receive the tokens. + * @param _ids The token types to mint. + * @param _values The amounts of tokens to mint for each type. + * @param _data Additional data with no specified format. + */ +function mintBatch(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) { + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + for (uint256 i = 0; i < _ids.length; i++) { + s.balanceOf[_ids[i]][_to] += _values[i]; + } + + emit TransferBatch(msg.sender, address(0), _to, _ids, _values); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155BatchReceived(msg.sender, address(0), _ids, _values, _data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { + revert ERC1155InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } +} diff --git a/cli/project-template/src/templates/token/ERC1155/Transfer/ERC1155TransferFacet.sol b/cli/project-template/src/templates/token/ERC1155/Transfer/ERC1155TransferFacet.sol new file mode 100644 index 00000000..ccd1129b --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Transfer/ERC1155TransferFacet.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Token Receiver Interface + * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. + */ +interface IERC1155Receiver { + /** + * @notice Handles the receipt of a single ERC-1155 token type. + * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. + * + * IMPORTANT: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param _operator The address which initiated the transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _id The ID of the token being transferred. + * @param _value The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed. + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) + external + returns (bytes4); + + /** + * @notice Handles the receipt of multiple ERC-1155 token types. + * @dev This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. + * + * IMPORTANT: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param _operator The address which initiated the batch transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _ids An array containing ids of each token being transferred (order and length must match _values array). + * @param _values An array containing amounts of each token being transferred (order and length must match _ids array). + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed. + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} + +/** + * @title ERC-1155 Transfer Facet + * @notice Provides transfer functionality for ERC-1155 tokens. + */ +contract ERC1155TransferFacet { + /** + * @notice Error indicating insufficient balance for a transfer. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + * @param _tokenId The token ID involved. + */ + error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + + /** + * @notice Error indicating the sender address is invalid. + * @param _sender Invalid sender address. + */ + error ERC1155InvalidSender(address _sender); + + /** + * @notice Error indicating the receiver address is invalid. + * @param _receiver Invalid receiver address. + */ + error ERC1155InvalidReceiver(address _receiver); + + /** + * @notice Error indicating missing approval for an operator. + * @param _operator Address attempting the operation. + * @param _owner The token owner. + */ + error ERC1155MissingApprovalForAll(address _operator, address _owner); + + /** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ + error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + + /** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ + event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value + ); + + /** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ + event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values + ); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ + struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; + } + + /** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ + function getStorage() internal pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Transfers `value` amount of token type `id` from `from` to `to`. + * @dev Emits a {TransferSingle} event. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _id The token type to transfer. + * @param _value The amount to transfer. + * @param _data Additional data with no specified format. + */ + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external { + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + + ERC1155Storage storage s = getStorage(); + + /** + * Check authorization + */ + if (_from != msg.sender && !s.isApprovedForAll[_from][msg.sender]) { + revert ERC1155MissingApprovalForAll(msg.sender, _from); + } + + uint256 fromBalance = s.balanceOf[_id][_from]; + + if (fromBalance < _value) { + revert ERC1155InsufficientBalance(_from, fromBalance, _value, _id); + } + + unchecked { + s.balanceOf[_id][_from] = fromBalance - _value; + } + s.balanceOf[_id][_to] += _value; + + emit TransferSingle(msg.sender, _from, _to, _id, _value); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert ERC1155InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } + } + + /** + * @notice Batched version of {safeTransferFrom}. + * @dev Emits a {TransferBatch} event. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _ids The token types to transfer. + * @param _values The amounts to transfer. + * @param _data Additional data with no specified format. + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external { + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + /** + * Check authorization + */ + if (_from != msg.sender && !s.isApprovedForAll[_from][msg.sender]) { + revert ERC1155MissingApprovalForAll(msg.sender, _from); + } + + for (uint256 i = 0; i < _ids.length; i++) { + uint256 id = _ids[i]; + uint256 value = _values[i]; + uint256 fromBalance = s.balanceOf[id][_from]; + + if (fromBalance < value) { + revert ERC1155InsufficientBalance(_from, fromBalance, value, id); + } + + unchecked { + s.balanceOf[id][_from] = fromBalance - value; + } + s.balanceOf[id][_to] += value; + } + + emit TransferBatch(msg.sender, _from, _to, _ids, _values); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155BatchReceived(msg.sender, _from, _ids, _values, _data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { + revert ERC1155InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } + } + + /** + * @notice Exports the function selectors of the ERC1155TransferFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155TransferFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.safeTransferFrom.selector, this.safeBatchTransferFrom.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC1155/Transfer/ERC1155TransferMod.sol b/cli/project-template/src/templates/token/ERC1155/Transfer/ERC1155TransferMod.sol new file mode 100644 index 00000000..bcfb0e70 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC1155/Transfer/ERC1155TransferMod.sol @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Token Receiver Interface + * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. + */ +interface IERC1155Receiver { + /** + * @notice Handles the receipt of a single ERC-1155 token type. + * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. + * + * IMPORTANT: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param _operator The address which initiated the transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _id The ID of the token being transferred. + * @param _value The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed. + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) + external + returns (bytes4); + + /** + * @notice Handles the receipt of multiple ERC-1155 token types. + * @dev This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. + * + * IMPORTANT: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param _operator The address which initiated the batch transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _ids An array containing ids of each token being transferred (order and length must match _values array). + * @param _values An array containing amounts of each token being transferred (order and length must match _ids array). + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed. + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} + +/** + * @title ERC-1155 Transfer Module + * @notice Provides internal transfer functionality for ERC-1155 tokens. + */ + +/** + * @notice Error indicating insufficient balance for a transfer. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + * @param _tokenId The token ID involved. + */ +error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + +/** + * @notice Error indicating the sender address is invalid. + * @param _sender Invalid sender address. + */ +error ERC1155InvalidSender(address _sender); + +/** + * @notice Error indicating the receiver address is invalid. + * @param _receiver Invalid receiver address. + */ +error ERC1155InvalidReceiver(address _receiver); + +/** + * @notice Error indicating missing approval for an operator. + * @param _operator Address attempting the operation. + * @param _owner The token owner. + */ +error ERC1155MissingApprovalForAll(address _operator, address _owner); + +/** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ +error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + +/** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ +event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value +); + +/** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ +event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values +); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ +struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; +} + +/** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ +function getStorage() pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Safely transfers a single token type from one address to another. + * @dev Validates ownership, approval, and receiver address before updating balances. + * Performs ERC1155Receiver validation if recipient is a contract (safe transfer). + * Complies with EIP-1155 safe transfer requirements. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _id The token type to transfer. + * @param _value The amount of tokens to transfer. + * @param _operator The address initiating the transfer (may be owner or approved operator). + */ +function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, address _operator) { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + + ERC1155Storage storage s = getStorage(); + + /** + * Check authorization + */ + if (_from != _operator && !s.isApprovedForAll[_from][_operator]) { + revert ERC1155MissingApprovalForAll(_operator, _from); + } + + uint256 fromBalance = s.balanceOf[_id][_from]; + + if (fromBalance < _value) { + revert ERC1155InsufficientBalance(_from, fromBalance, _value, _id); + } + + unchecked { + s.balanceOf[_id][_from] = fromBalance - _value; + } + s.balanceOf[_id][_to] += _value; + + emit TransferSingle(_operator, _from, _to, _id, _value); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155Received(_operator, _from, _id, _value, "") returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert ERC1155InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } +} + +/** + * @notice Safely transfers multiple token types from one address to another in a single transaction. + * @dev Validates ownership, approval, and receiver address before updating balances for each token type. + * Performs ERC1155Receiver validation if recipient is a contract (safe transfer). + * Complies with EIP-1155 safe transfer requirements. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _ids The token types to transfer. + * @param _values The amounts of tokens to transfer for each type. + * @param _operator The address initiating the transfer (may be owner or approved operator). + */ +function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _values, + address _operator +) { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + /** + * Check authorization + */ + if (_from != _operator && !s.isApprovedForAll[_from][_operator]) { + revert ERC1155MissingApprovalForAll(_operator, _from); + } + + for (uint256 i = 0; i < _ids.length; i++) { + uint256 id = _ids[i]; + uint256 value = _values[i]; + uint256 fromBalance = s.balanceOf[id][_from]; + + if (fromBalance < value) { + revert ERC1155InsufficientBalance(_from, fromBalance, value, id); + } + + unchecked { + s.balanceOf[id][_from] = fromBalance - value; + } + s.balanceOf[id][_to] += value; + } + + emit TransferBatch(_operator, _from, _to, _ids, _values); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155BatchReceived(_operator, _from, _ids, _values, "") returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { + revert ERC1155InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Approve/ERC20ApproveFacet.sol b/cli/project-template/src/templates/token/ERC20/Approve/ERC20ApproveFacet.sol new file mode 100644 index 00000000..a4914192 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Approve/ERC20ApproveFacet.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC20ApproveFacet { + /** + * @notice Thrown when the spender address is invalid (e.g., zero address). + * @param _spender Invalid spender address. + */ + error ERC20InvalidSpender(address _spender); + + /** + * @notice Emitted when an approval is made for a spender by an owner. + * @param _owner The address granting the allowance. + * @param _spender The address receiving the allowance. + * @param _value The amount approved. + */ + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc20"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20 + */ + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + } + + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Approves a spender to transfer up to a certain amount of tokens on behalf of the caller. + * @dev Emits an {Approval} event. + * @param _spender The address approved to spend tokens. + * @param _value The number of tokens to approve. + * @return True if the approval was successful. + */ + function approve(address _spender, uint256 _value) external returns (bool) { + ERC20Storage storage s = getStorage(); + if (_spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + s.allowance[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + /** + * @notice Exports the function selectors of the ERC20ApproveFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC20ApproveFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.approve.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Approve/ERC20ApproveMod.sol b/cli/project-template/src/templates/token/ERC20/Approve/ERC20ApproveMod.sol new file mode 100644 index 00000000..de4b310e --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Approve/ERC20ApproveMod.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the spender address is invalid (e.g., zero address). + * @param _spender Invalid spender address. + */ +error ERC20InvalidSpender(address _spender); + +/** + * @notice Emitted when an approval is made for a spender by an owner. + * @param _owner The address granting the allowance. + * @param _spender The address receiving the allowance. + * @param _value The amount approved. + */ +event Approval(address indexed _owner, address indexed _spender, uint256 _value); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc20"); + +/** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20 + */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +} + +/** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ +function getStorage() pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Approves a spender to transfer tokens on behalf of the caller. + * @dev Sets the allowance for the spender. + * @param _spender The address to approve for spending. + * @param _value The amount of tokens to approve. + * @return True if the approval was successful. + */ +function approve(address _spender, uint256 _value) returns (bool) { + if (_spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + ERC20Storage storage s = getStorage(); + s.allowance[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; +} diff --git a/cli/project-template/src/templates/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol b/cli/project-template/src/templates/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol new file mode 100644 index 00000000..634113e1 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ +/** + * @title ERC20Bridgeable — ERC-7802 Implementation Facet + * @notice Provides functions and storage layout for ERC20-Bridgeable token logic. + * @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions + */ + +contract ERC20BridgeableFacet { + /** + * @notice Revert when a provided receiver is invalid(e.g,zero address) . + * @param _receiver The invalid receiver address. + */ + error ERC20InvalidReceiver(address _receiver); + + /** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender The invalid sender address. + */ + error ERC20InvalidSender(address _sender); + + /** + * @notice Revert when caller is not a trusted bridge. + * @param _caller The unauthorized caller. + */ + error ERC20InvalidBridgeAccount(address _caller); + + /** + * @notice Revert when caller address is invalid. + */ + /** + * @param _caller is the invalid address. + */ + error ERC20InvalidCallerAddress(address _caller); + + /** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ + error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + + error ERC20InsufficientBalance(address _from, uint256 _accountBalance, uint256 _value); + + /** + * @notice Emitted when tokens are minted via a cross-chain bridge. + * @param _to The recipient of minted tokens. + * @param _amount The amount minted. + * @param _sender The bridge account that triggered the mint (msg.sender). + */ + event CrosschainMint(address indexed _to, uint256 _amount, address indexed _sender); + + /** + * @notice Emitted when a crosschain transfer burns tokens. + * @param _from Address of the account tokens are being burned from. + * @param _amount Amount of tokens burned. + * @param _sender Address of the caller (msg.sender) who invoked crosschainBurn. + */ + event CrosschainBurn(address indexed _from, uint256 _amount, address indexed _sender); + + /** + * @notice Emitted when tokens are transferred between two addresses. + * @param _from Address sending the tokens. + * @param _to Address receiving the tokens. + * @param _value Amount of tokens transferred. + */ + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + /** + * ----------------------------------------------------------------------- + * ERC20 integration (re-uses ERC20Facet storage layout) + * ----------------------------------------------------------------------- + */ + + /** + * @notice Storage slot for ERC-20 token using ERC8042 for storage location standardization + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant ERC20_STORAGE_POSITION = keccak256("erc20"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20 + */ + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + } + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + + function getERC20Storage() internal pure returns (ERC20Storage storage s) { + bytes32 position = ERC20_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * ----------------------------------------------------------------------- + * AccessControl integration (re-uses AccessControlFacet storage layout) + * ----------------------------------------------------------------------- + */ + + /** + * @notice Storage slot identifier. + */ + bytes32 constant ACCESS_STORAGE_POSITION = keccak256("compose.accesscontrol"); + + /** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ + struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + } + + /** + * @notice helper to return AccessControlStorage at its diamond slot + */ + function getAccessControlStorage() internal pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Cross-chain mint — callable only by an address having the `trusted-bridge` role. + * @param _account The account to mint tokens to. + * @param _value The amount to mint. + */ + function crosschainMint(address _account, uint256 _value) external { + ERC20Storage storage erc20Storage = getERC20Storage(); + + AccessControlStorage storage acs = getAccessControlStorage(); + + /** + * authorize: caller must have the trusted-bridge role + */ + if (!acs.hasRole[msg.sender]["trusted-bridge"]) { + revert AccessControlUnauthorizedAccount(msg.sender, "trusted-bridge"); + } + + if (_account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + + unchecked { + erc20Storage.totalSupply += _value; + erc20Storage.balanceOf[_account] += _value; + } + emit Transfer(address(0), _account, _value); + emit CrosschainMint(_account, _value, msg.sender); + } + + /** + * @notice Cross-chain burn — callable only by an address having the `trusted-bridge` role. + * @param _from The account to burn tokens from. + * @param _value The amount to burn. + */ + function crosschainBurn(address _from, uint256 _value) external { + ERC20Storage storage erc20Storage = getERC20Storage(); + + AccessControlStorage storage acs = getAccessControlStorage(); + + /** + * authorize: caller must have the trusted-bridge role + */ + if (!acs.hasRole[msg.sender]["trusted-bridge"]) { + revert AccessControlUnauthorizedAccount(msg.sender, "trusted-bridge"); + } + if (_from == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + + uint256 accountBalance = erc20Storage.balanceOf[_from]; + + if (accountBalance < _value) { + revert ERC20InsufficientBalance(_from, accountBalance, _value); + } + + unchecked { + erc20Storage.totalSupply -= _value; + erc20Storage.balanceOf[_from] -= _value; + } + + emit Transfer(_from, address(0), _value); + emit CrosschainBurn(_from, _value, msg.sender); + } + + /** + * @notice Internal check to check if the bridge (caller) is trusted. + * @dev Reverts if caller is zero or not in the AccessControl `trusted-bridge` role. + * @param _caller The address to validate + */ + function checkTokenBridge(address _caller) external view { + AccessControlStorage storage acs = getAccessControlStorage(); + + if (_caller == address(0)) { + revert ERC20InvalidBridgeAccount(address(0)); + } + + if (!acs.hasRole[_caller]["trusted-bridge"]) { + revert ERC20InvalidBridgeAccount(_caller); + } + } + + /** + * @notice Exports the function selectors of the ERC20BridgeableFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC20BridgeableFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.crosschainMint.selector, this.crosschainBurn.selector, this.checkTokenBridge.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Bridgeable/ERC20BridgeableMod.sol b/cli/project-template/src/templates/token/ERC20/Bridgeable/ERC20BridgeableMod.sol new file mode 100644 index 00000000..2ebf36ba --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Bridgeable/ERC20BridgeableMod.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title LibERC20Bridgeable — ERC-7802 Library + * @notice Provides internal functions and storage layout for ERC-7802 token logic. + * @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions + */ + +/** + * @notice Revert when a provided receiver is invalid(e.g,zero address) . + * @param _receiver The invalid receiver address. + */ +error ERC20InvalidReceiver(address _receiver); + +/** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender The invalid sender address. + */ +error ERC20InvalidSender(address _sender); + +/** + * @notice Revert when caller is not a trusted bridge. + * @param _caller The unauthorized caller. + */ +error ERC20InvalidBridgeAccount(address _caller); + +/** + * @notice Revert when caller address is invalid. + */ +/** + * @param _caller is the invalid address. + */ +error ERC20InvalidCallerAddress(address _caller); + +/** + * @notice Thrown when the account does not have a specific role. + * @param _role The role that the account does not have. + * @param _account The account that does not have the role. + */ +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +error ERC20InsufficientBalance(address _from, uint256 _accountBalance, uint256 _value); + +/** + * @notice Emitted when tokens are minted via a cross-chain bridge. + * @param _to The recipient of minted tokens. + * @param _amount The amount minted. + * @param _sender The bridge account that triggered the mint (msg.sender). + */ +event CrosschainMint(address indexed _to, uint256 _amount, address indexed _sender); + +/** + * @notice Emitted when a crosschain transfer burns tokens. + * @param _from Address of the account tokens are being burned from. + * @param _amount Amount of tokens burned. + * @param _sender Address of the caller (msg.sender) who invoked crosschainBurn. + */ +event CrosschainBurn(address indexed _from, uint256 _amount, address indexed _sender); + +/** + * @notice Emitted when tokens are transferred between two addresses. + * @param _from Address sending the tokens. + * @param _to Address receiving the tokens. + * @param _value Amount of tokens transferred. + */ +event Transfer(address indexed _from, address indexed _to, uint256 _value); + +/* + * ----------------------------------------------------------------------- + * ERC20 integration (re-uses ERC20Facet storage layout) + * ----------------------------------------------------------------------- + */ + +/* + * @notice Storage slot for ERC-20 token using ERC8042 for storage location standardization + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant ERC20_STORAGE_POSITION = keccak256("erc20"); + +/** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20 + */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; +} +/** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + +function getERC20Storage() pure returns (ERC20Storage storage s) { + bytes32 position = ERC20_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/* + * ----------------------------------------------------------------------- + * AccessControl integration (re-uses AccessControlFacet storage layout) + * ----------------------------------------------------------------------- + */ + +/* + * @notice Storage slot identifier. + */ +bytes32 constant ACCESS_STORAGE_POSITION = keccak256("compose.accesscontrol"); + +/** + * @notice storage struct for the AccessControl. + * @custom:storage-location erc8042:compose.accesscontrol + */ +struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; +} + +/** + * @notice helper to return AccessControlStorage at its diamond slot + */ +function getAccessControlStorage() pure returns (AccessControlStorage storage s) { + bytes32 position = ACCESS_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Cross-chain mint — callable only by an address having the `trusted-bridge` role. + * @param _account The account to mint tokens to. + * @param _value The amount to mint. + */ +function crosschainMint(address _account, uint256 _value) { + ERC20Storage storage erc20Storage = getERC20Storage(); + + AccessControlStorage storage acs = getAccessControlStorage(); + + /** + * authorize: caller must have the trusted-bridge role + */ + if (!acs.hasRole[msg.sender]["trusted-bridge"]) { + revert AccessControlUnauthorizedAccount(msg.sender, "trusted-bridge"); + } + + if (_account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + + unchecked { + erc20Storage.totalSupply += _value; + erc20Storage.balanceOf[_account] += _value; + } + + emit Transfer(address(0), _account, _value); + emit CrosschainMint(_account, _value, msg.sender); +} + +/** + * @notice Cross-chain burn — callable only by an address having the `trusted-bridge` role. + * @param _from The account to burn tokens from. + * @param _value The amount to burn. + */ +function crosschainBurn(address _from, uint256 _value) { + ERC20Storage storage erc20Storage = getERC20Storage(); + + AccessControlStorage storage acs = getAccessControlStorage(); + + /** + * authorize: caller must have the trusted-bridge role + */ + if (!acs.hasRole[msg.sender]["trusted-bridge"]) { + revert AccessControlUnauthorizedAccount(msg.sender, "trusted-bridge"); + } + + if (_from == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + + uint256 accountBalance = erc20Storage.balanceOf[_from]; + + if (accountBalance < _value) { + revert ERC20InsufficientBalance(_from, accountBalance, _value); + } + + unchecked { + erc20Storage.totalSupply -= _value; + erc20Storage.balanceOf[_from] -= _value; + } + emit Transfer(_from, address(0), _value); + emit CrosschainBurn(_from, _value, msg.sender); +} + +/** + * @notice Internal check to check if the bridge (caller) is trusted. + * @dev Reverts if caller is zero or not in the AccessControl `trusted-bridge` role. + * @param _caller The address to validate + */ +function checkTokenBridge(address _caller) view { + AccessControlStorage storage acs = getAccessControlStorage(); + + if (_caller == address(0)) { + revert ERC20InvalidBridgeAccount(address(0)); + } + + if (!acs.hasRole[_caller]["trusted-bridge"]) { + revert ERC20InvalidBridgeAccount(_caller); + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Burn/ERC20BurnFacet.sol b/cli/project-template/src/templates/token/ERC20/Burn/ERC20BurnFacet.sol new file mode 100644 index 00000000..610a0b02 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Burn/ERC20BurnFacet.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/** + * Compose + * https://compose.diamonds + */ + +contract ERC20BurnFacet { + /** + * @notice Thrown when an account has insufficient balance for a transfer or burn. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + */ + error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + + /** + * @notice Thrown when a spender tries to use more than the approved allowance. + * @param _spender Address attempting to spend. + * @param _allowance Current allowance for the spender. + * @param _needed Amount required to complete the operation. + */ + error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + + /** + * @notice Emitted when tokens are transferred between two addresses. + * @param _from Address sending the tokens. + * @param _to Address receiving the tokens. + * @param _value Amount of tokens transferred. + */ + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc20"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20 + */ + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + } + + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Burns (destroys) a specific amount of tokens from the caller's balance. + * @dev Emits a {Transfer} event to the zero address. + * @param _value The amount of tokens to burn. + */ + function burn(uint256 _value) external { + ERC20Storage storage s = getStorage(); + uint256 balance = s.balanceOf[msg.sender]; + if (balance < _value) { + revert ERC20InsufficientBalance(msg.sender, balance, _value); + } + unchecked { + s.balanceOf[msg.sender] = balance - _value; + s.totalSupply -= _value; + } + emit Transfer(msg.sender, address(0), _value); + } + + /** + * @notice Burns tokens from another account, deducting from the caller's allowance. + * @dev Emits a {Transfer} event to the zero address. + * @param _account The address whose tokens will be burned. + * @param _value The amount of tokens to burn. + */ + function burnFrom(address _account, uint256 _value) external { + ERC20Storage storage s = getStorage(); + uint256 currentAllowance = s.allowance[_account][msg.sender]; + if (currentAllowance < _value) { + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + } + uint256 balance = s.balanceOf[_account]; + if (balance < _value) { + revert ERC20InsufficientBalance(_account, balance, _value); + } + unchecked { + if (currentAllowance != type(uint256).max) { + s.allowance[_account][msg.sender] = currentAllowance - _value; + } + s.balanceOf[_account] = balance - _value; + s.totalSupply -= _value; + } + emit Transfer(_account, address(0), _value); + } + + /** + * @notice Exports the function selectors of the ERC20BurnFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC20BurnFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.burn.selector, this.burnFrom.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Burn/ERC20BurnMod.sol b/cli/project-template/src/templates/token/ERC20/Burn/ERC20BurnMod.sol new file mode 100644 index 00000000..f3f15c42 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Burn/ERC20BurnMod.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC20BurnMod + * @notice Provides internal functions for burning ERC-20 tokens. + * @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions. + */ + +/** + * @notice Thrown when a sender attempts to transfer or burn more tokens than their balance. + * @param _sender The address attempting the transfer or burn. + * @param _balance The sender's current balance. + * @param _needed The amount required to complete the operation. + */ +error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + +/** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender The invalid sender address. + */ +error ERC20InvalidSender(address _sender); + +/** + * @notice Emitted when tokens are transferred between addresses. + * @param _from The address tokens are transferred from. + * @param _to The address tokens are transferred to. + * @param _value The amount of tokens transferred. + */ +event Transfer(address indexed _from, address indexed _to, uint256 _value); + +/* + * @notice Storage slot identifier, defined using keccak256 hash of the library diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc20"); + +/** + * @notice ERC-20 storage layout using the ERC-8042 standard. + * @custom:storage-location erc8042:erc20 + */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; +} + +/** + * @notice Returns a pointer to the ERC-20 storage struct. + * @dev Uses inline assembly to bind the storage struct to the fixed storage position. + * @return s The ERC-20 storage struct. + */ +function getStorage() pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Burns tokens from a specified address. + * @dev Decreases both total supply and the sender's balance. + * This module does not perform allowance checks. Ensure proper allowance or authorization validation before calling this function. + * @param _account The address whose tokens will be burned. + * @param _value The number of tokens to burn. + */ +function burn(address _account, uint256 _value) { + ERC20Storage storage s = getStorage(); + if (_account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + uint256 accountBalance = s.balanceOf[_account]; + if (accountBalance < _value) { + revert ERC20InsufficientBalance(_account, accountBalance, _value); + } + unchecked { + s.balanceOf[_account] = accountBalance - _value; + s.totalSupply -= _value; + } + emit Transfer(_account, address(0), _value); +} diff --git a/cli/project-template/src/templates/token/ERC20/Data/ERC20DataFacet.sol b/cli/project-template/src/templates/token/ERC20/Data/ERC20DataFacet.sol new file mode 100644 index 00000000..ad2b7560 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Data/ERC20DataFacet.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC20DataFacet { + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc20"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20 + */ + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + } + + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the total supply of tokens. + * @return The total token supply. + */ + function totalSupply() external view returns (uint256) { + return getStorage().totalSupply; + } + + /** + * @notice Returns the balance of a specific account. + * @param _account The address of the account. + * @return The account balance. + */ + function balanceOf(address _account) external view returns (uint256) { + return getStorage().balanceOf[_account]; + } + + /** + * @notice Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner. + * @param _owner The address of the token owner. + * @param _spender The address of the spender. + * @return The remaining allowance. + */ + function allowance(address _owner, address _spender) external view returns (uint256) { + return getStorage().allowance[_owner][_spender]; + } + + /** + * @notice Exports the function selectors of the ERC20Data facet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC20DataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.totalSupply.selector, this.balanceOf.selector, this.allowance.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Metadata/ERC20MetadataFacet.sol b/cli/project-template/src/templates/token/ERC20/Metadata/ERC20MetadataFacet.sol new file mode 100644 index 00000000..1c3cb660 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Metadata/ERC20MetadataFacet.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC20MetadataFacet { + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc20.metadata"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20.metadata + */ + struct ERC20MetadataStorage { + string name; + string symbol; + uint8 decimals; + } + + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the name of the token. + * @return The token name. + */ + function name() external view returns (string memory) { + return getStorage().name; + } + + /** + * @notice Returns the symbol of the token. + * @return The token symbol. + */ + function symbol() external view returns (string memory) { + return getStorage().symbol; + } + + /** + * @notice Returns the number of decimals used for token precision. + * @return The number of decimals. + */ + function decimals() external view returns (uint8) { + return getStorage().decimals; + } + + /** + * @notice Exports the function selectors of the ERC20Metadata facet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC20MetadataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.name.selector, this.symbol.selector, this.decimals.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Metadata/ERC20MetadataMod.sol b/cli/project-template/src/templates/token/ERC20/Metadata/ERC20MetadataMod.sol new file mode 100644 index 00000000..d7c879bb --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Metadata/ERC20MetadataMod.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc20.metadata"); + +/** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20.metadata + */ +struct ERC20MetadataStorage { + string name; + string symbol; + uint8 decimals; +} + +/** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ +function getStorage() pure returns (ERC20MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Sets the metadata for the ERC20 token. + * @param _name The name of the token. + * @param _symbol The symbol of the token. + * @param _decimals The number of decimals used for token precision. + */ +function setMetadata(string memory _name, string memory _symbol, uint8 _decimals) { + ERC20MetadataStorage storage s = getStorage(); + s.name = _name; + s.symbol = _symbol; + s.decimals = _decimals; +} + diff --git a/cli/project-template/src/templates/token/ERC20/Mint/ERC20MintMod.sol b/cli/project-template/src/templates/token/ERC20/Mint/ERC20MintMod.sol new file mode 100644 index 00000000..5e0ed456 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Mint/ERC20MintMod.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC20MintMod + * @notice Provides internal functions for minting ERC-20 tokens. + * @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions. + */ + +/** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver The invalid receiver address. + */ +error ERC20InvalidReceiver(address _receiver); + +/** + * @notice Emitted when tokens are transferred between addresses. + * @param _from The address tokens are transferred from. + * @param _to The address tokens are transferred to. + * @param _value The amount of tokens transferred. + */ +event Transfer(address indexed _from, address indexed _to, uint256 _value); + +/* + * @notice Storage slot identifier, defined using keccak256 hash of the library diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc20"); + +/* + * @notice ERC-20 storage layout using the ERC-8042 standard. + * @custom:storage-location erc8042:erc20 + */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; +} + +/** + * @notice Returns a pointer to the ERC-20 storage struct. + * @dev Uses inline assembly to bind the storage struct to the fixed storage position. + * @return s The ERC-20 storage struct. + */ +function getStorage() pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Mints new tokens to a specified address. + * @dev Increases both total supply and the recipient's balance. + * @param _account The address receiving the newly minted tokens. + * @param _value The number of tokens to mint. + */ +function mint(address _account, uint256 _value) { + ERC20Storage storage s = getStorage(); + if (_account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + s.totalSupply += _value; + s.balanceOf[_account] += _value; + emit Transfer(address(0), _account, _value); +} diff --git a/cli/project-template/src/templates/token/ERC20/Permit/ERC20PermitFacet.sol b/cli/project-template/src/templates/token/ERC20/Permit/ERC20PermitFacet.sol new file mode 100644 index 00000000..cba8b74d --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Permit/ERC20PermitFacet.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC20PermitFacet + * @notice Facet for ERC20 permit functionality + * @dev Implements EIP-2612: https://eips.ethereum.org/EIPS/eip-2612 + */ +contract ERC20PermitFacet { + /** + * @notice Thrown when a permit signature is invalid or expired. + * @param _owner The address that signed the permit. + * @param _spender The address that was approved. + * @param _value The amount that was approved. + * @param _deadline The deadline for the permit. + * @param _v The recovery byte of the signature. + * @param _r The r value of the signature. + * @param _s The s value of the signature. + */ + error ERC2612InvalidSignature( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s + ); + + /** + * @notice Thrown when the spender address is invalid (e.g., zero address). + * @param _spender Invalid spender address. + */ + error ERC20InvalidSpender(address _spender); + + /** + * @notice Emitted when an approval is made for a spender by an owner. + * @param _owner The address granting the allowance. + * @param _spender The address receiving the allowance. + * @param _value The amount approved. + */ + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + bytes32 constant ERC20_METADATA_STORAGE_POSITION = keccak256("erc20.metadata"); + + /** + * @custom:storage-location erc8042:erc20.metadata + */ + struct ERC20MetadataStorage { + string name; + } + + function getERC20MetadataStorage() internal pure returns (ERC20MetadataStorage storage s) { + bytes32 position = ERC20_METADATA_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + bytes32 constant ERC20_STORAGE_POSITION = keccak256("erc20"); + + /** + * @custom:storage-location erc8042:erc20 + */ + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + } + + function getERC20Storage() internal pure returns (ERC20Storage storage s) { + bytes32 position = ERC20_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + bytes32 constant STORAGE_POSITION = keccak256("nonces"); + + /** + * @custom:storage-location erc8042:nonces + */ + struct NoncesStorage { + mapping(address owner => uint256) nonces; + } + + function getStorage() internal pure returns (NoncesStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the current nonce for an owner. + * @dev This value changes each time a permit is used. + * @param _owner The address of the owner. + * @return The current nonce. + */ + function nonces(address _owner) external view returns (uint256) { + return getStorage().nonces[_owner]; + } + + /** + * @notice Returns the domain separator used in the encoding of the signature for {permit}. + * @dev This value is unique to a contract and chain ID combination to prevent replay attacks. + * @return The domain separator. + */ + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(getERC20MetadataStorage().name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } + + /** + * @notice Sets the allowance for a spender via a signature. + * @dev This function implements EIP-2612 permit functionality. + * @param _owner The address of the token owner. + * @param _spender The address of the spender. + * @param _value The amount of tokens to approve. + * @param _deadline The deadline for the permit (timestamp). + * @param _v The recovery byte of the signature. + * @param _r The r value of the signature. + * @param _s The s value of the signature. + */ + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external { + if (_spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + if (block.timestamp > _deadline) { + revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); + } + + NoncesStorage storage s = getStorage(); + ERC20Storage storage erc20Storage = getERC20Storage(); + uint256 currentNonce = s.nonces[_owner]; + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + _owner, + _spender, + _value, + currentNonce, + _deadline + ) + ); + + bytes32 hash = keccak256( + abi.encodePacked( + "\x19\x01", + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(getERC20MetadataStorage().name)), + keccak256("1"), + block.chainid, + address(this) + ) + ), + structHash + ) + ); + + address signer = ecrecover(hash, _v, _r, _s); + if (signer != _owner || signer == address(0)) { + revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); + } + + erc20Storage.allowance[_owner][_spender] = _value; + s.nonces[_owner] = currentNonce + 1; + emit Approval(_owner, _spender, _value); + } + + /** + * @notice Exports the function selectors of the ERC20PermitFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC20PermitFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.nonces.selector, this.DOMAIN_SEPARATOR.selector, this.permit.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Permit/ERC20PermitMod.sol b/cli/project-template/src/templates/token/ERC20/Permit/ERC20PermitMod.sol new file mode 100644 index 00000000..8f7a633c --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Permit/ERC20PermitMod.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title LibERC20Permit — Library for ERC-2612 Permit Logic + * @notice Library for self-contained ERC-2612 permit and domain separator logic and storage + * @dev Does not import anything. Designed to be used by facets handling ERC20 permit functionality. + */ + +/** + * @notice Thrown when a permit signature is invalid or expired. + * @param _owner The address that signed the permit. + * @param _spender The address that was approved. + * @param _value The amount that was approved. + * @param _deadline The deadline for the permit. + * @param _v The recovery byte of the signature. + * @param _r The r value of the signature. + * @param _s The s value of the signature. + */ +error ERC2612InvalidSignature( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s +); + +/** + * @notice Thrown when the spender address is invalid (e.g., zero address). + * @param _spender Invalid spender address. + */ +error ERC20InvalidSpender(address _spender); + +/** + * @notice Emitted when an approval is made for a spender by an owner. + * @param _owner The address granting the allowance. + * @param _spender The address receiving the allowance. + * @param _value The amount approved. + */ +event Approval(address indexed _owner, address indexed _spender, uint256 _value); + +bytes32 constant ERC20_METADATA_STORAGE_POSITION = keccak256("erc20.metadata"); + +/** + * @custom:storage-location erc8042:erc20.metadata + */ +struct ERC20MetadataStorage { + string name; +} + +function getERC20MetadataStorage() pure returns (ERC20MetadataStorage storage s) { + bytes32 position = ERC20_METADATA_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +bytes32 constant ERC20_STORAGE_POSITION = keccak256("erc20"); + +/** + * @custom:storage-location erc8042:erc20 + */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +} + +function getERC20Storage() pure returns (ERC20Storage storage s) { + bytes32 position = ERC20_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +bytes32 constant STORAGE_POSITION = keccak256("nonces"); + +/** + * @custom:storage-location erc8042:nonces + */ +struct NoncesStorage { + mapping(address owner => uint256) nonces; +} + +function getPermitStorage() pure returns (NoncesStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the domain separator used in the encoding of the signature for {permit}. + * @dev This value is unique to a contract and chain ID combination to prevent replay attacks. + * @return The domain separator. + */ +function DOMAIN_SEPARATOR() view returns (bytes32) { + address thisAddress; + assembly { + thisAddress := address() + } + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(getERC20MetadataStorage().name)), + keccak256("1"), + block.chainid, + thisAddress + ) + ); +} + +/** + * @notice Validates a permit signature and sets allowance. + * @dev Emits Approval event; must be emitted by the calling facet/contract. + * @param _owner Token owner. + * @param _spender Token spender. + * @param _value Allowance value. + * @param _deadline Permit's time deadline. + * @param _v, _r, _s Signature fields. + */ +function permit(address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s) { + if (_spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + if (block.timestamp > _deadline) { + revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); + } + + NoncesStorage storage s = getPermitStorage(); + ERC20Storage storage erc20Storage = getERC20Storage(); + uint256 currentNonce = s.nonces[_owner]; + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + _owner, + _spender, + _value, + currentNonce, + _deadline + ) + ); + address thisAddress; + assembly { + thisAddress := address() + } + bytes32 hash = keccak256( + abi.encodePacked( + "\x19\x01", + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(getERC20MetadataStorage().name)), + keccak256("1"), + block.chainid, + thisAddress + ) + ), + structHash + ) + ); + + address signer = ecrecover(hash, _v, _r, _s); + if (signer != _owner || signer == address(0)) { + revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); + } + + erc20Storage.allowance[_owner][_spender] = _value; + s.nonces[_owner] = currentNonce + 1; + emit Approval(_owner, _spender, _value); +} diff --git a/cli/project-template/src/templates/token/ERC20/Transfer/ERC20TransferFacet.sol b/cli/project-template/src/templates/token/ERC20/Transfer/ERC20TransferFacet.sol new file mode 100644 index 00000000..19bb51b6 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Transfer/ERC20TransferFacet.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC20TransferFacet { + /** + * @notice Thrown when an account has insufficient balance for a transfer or burn. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + */ + error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + + /** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender Invalid sender address. + */ + error ERC20InvalidSender(address _sender); + + /** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver Invalid receiver address. + */ + error ERC20InvalidReceiver(address _receiver); + + /** + * @notice Thrown when a spender tries to use more than the approved allowance. + * @param _spender Address attempting to spend. + * @param _allowance Current allowance for the spender. + * @param _needed Amount required to complete the operation. + */ + error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + + /** + * @notice Thrown when the spender address is invalid (e.g., zero address). + * @param _spender Invalid spender address. + */ + error ERC20InvalidSpender(address _spender); + + /** + * @notice Emitted when tokens are transferred between two addresses. + * @param _from Address sending the tokens. + * @param _to Address receiving the tokens. + * @param _value Amount of tokens transferred. + */ + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc20"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:erc20 + */ + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + } + + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Transfers tokens to another address. + * @dev Emits a {Transfer} event. + * @param _to The address to receive the tokens. + * @param _value The amount of tokens to transfer. + * @return True if the transfer was successful. + */ + function transfer(address _to, uint256 _value) external returns (bool) { + ERC20Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 fromBalance = s.balanceOf[msg.sender]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(msg.sender, fromBalance, _value); + } + unchecked { + s.balanceOf[msg.sender] = fromBalance - _value; + } + s.balanceOf[_to] += _value; + emit Transfer(msg.sender, _to, _value); + return true; + } + + /** + * @notice Transfers tokens on behalf of another account, provided sufficient allowance exists. + * @dev Emits a {Transfer} event and decreases the spender's allowance. + * @param _from The address to transfer tokens from. + * @param _to The address to transfer tokens to. + * @param _value The amount of tokens to transfer. + * @return True if the transfer was successful. + */ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + ERC20Storage storage s = getStorage(); + if (_from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 currentAllowance = s.allowance[_from][msg.sender]; + if (currentAllowance < _value) { + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + } + uint256 fromBalance = s.balanceOf[_from]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(_from, fromBalance, _value); + } + unchecked { + if (currentAllowance != type(uint256).max) { + s.allowance[_from][msg.sender] = currentAllowance - _value; + } + s.balanceOf[_from] = fromBalance - _value; + } + s.balanceOf[_to] += _value; + emit Transfer(_from, _to, _value); + return true; + } + + /** + * @notice Exports the function selectors of the ERC20TransferFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC20TransferFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.transfer.selector, this.transferFrom.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC20/Transfer/ERC20TransferMod.sol b/cli/project-template/src/templates/token/ERC20/Transfer/ERC20TransferMod.sol new file mode 100644 index 00000000..0d446317 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC20/Transfer/ERC20TransferMod.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC20TransferMod + * @notice Provides transfer internal functions for ERC-20 tokens. + * @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions. + */ + +/** + * @notice Thrown when a sender attempts to transfer or burn more tokens than their balance. + * @param _sender The address attempting the transfer or burn. + * @param _balance The sender's current balance. + * @param _needed The amount required to complete the operation. + */ +error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + +/** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender The invalid sender address. + */ +error ERC20InvalidSender(address _sender); + +/** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver The invalid receiver address. + */ +error ERC20InvalidReceiver(address _receiver); + +/** + * @notice Thrown when a spender tries to spend more than their allowance. + * @param _spender The address attempting to spend. + * @param _allowance The current allowance. + * @param _needed The required amount to complete the transfer. + */ +error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + +/** + * @notice Thrown when the spender address is invalid (e.g., zero address). + * @param _spender The invalid spender address. + */ +error ERC20InvalidSpender(address _spender); + +/** + * @notice Emitted when tokens are transferred between addresses. + * @param _from The address tokens are transferred from. + * @param _to The address tokens are transferred to. + * @param _value The amount of tokens transferred. + */ +event Transfer(address indexed _from, address indexed _to, uint256 _value); + +/** + * @notice Emitted when an approval is made for a spender by an owner. + * @param _owner The address granting the allowance. + * @param _spender The address receiving the allowance. + * @param _value The amount approved. + */ +event Approval(address indexed _owner, address indexed _spender, uint256 _value); + +/* + * @notice Storage slot identifier, defined using keccak256 hash of the library diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc20"); + +/* + * @notice ERC-20 storage layout using the ERC-8042 standard. + * @custom:storage-location erc8042:erc20 + */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +} + +/** + * @notice Returns a pointer to the ERC-20 storage struct. + * @dev Uses inline assembly to bind the storage struct to the fixed storage position. + * @return s The ERC-20 storage struct. + */ +function getStorage() pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Transfers tokens from one address to another using an allowance. + * @dev Deducts the spender's allowance and updates balances. + * @param _from The address to send tokens from. + * @param _to The address to send tokens to. + * @param _value The number of tokens to transfer. + * @return True if the transfer was successful. + */ +function transferFrom(address _from, address _to, uint256 _value) returns (bool) { + ERC20Storage storage s = getStorage(); + if (_from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 currentAllowance = s.allowance[_from][msg.sender]; + if (currentAllowance < _value) { + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + } + uint256 fromBalance = s.balanceOf[_from]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(_from, fromBalance, _value); + } + unchecked { + if (currentAllowance != type(uint256).max) { + s.allowance[_from][msg.sender] = currentAllowance - _value; + } + s.balanceOf[_from] = fromBalance - _value; + } + s.balanceOf[_to] += _value; + emit Transfer(_from, _to, _value); + return true; +} + +/** + * @notice Transfers tokens from the caller to another address. + * @dev Updates balances directly without allowance mechanism. + * @param _to The address to send tokens to. + * @param _value The number of tokens to transfer. + * @return True if the transfer was successful. + */ +function transfer(address _to, uint256 _value) returns (bool) { + ERC20Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 fromBalance = s.balanceOf[msg.sender]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(msg.sender, fromBalance, _value); + } + unchecked { + s.balanceOf[msg.sender] = fromBalance - _value; + } + s.balanceOf[_to] += _value; + emit Transfer(msg.sender, _to, _value); + return true; +} + diff --git a/cli/project-template/src/templates/token/ERC6909/Approve/ERC6909ApproveFacet.sol b/cli/project-template/src/templates/token/ERC6909/Approve/ERC6909ApproveFacet.sol new file mode 100644 index 00000000..6bc82700 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Approve/ERC6909ApproveFacet.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC6909ApproveFacet { + /** + * @notice Thrown when the spender address is invalid. + */ + error ERC6909InvalidSpender(address _spender); + + /** + * @notice Emitted when an approval occurs. + */ + event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _amount); + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + + /** + * @custom:storage-location erc8042:erc6909 + */ + struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; + } + + /** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ + function getStorage() internal pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Approves an amount of an id to a spender. + * @param _spender The address of the spender. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the approval succeeded. + */ + function approve(address _spender, uint256 _id, uint256 _amount) external returns (bool) { + if (_spender == address(0)) { + revert ERC6909InvalidSpender(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + s.allowance[msg.sender][_spender][_id] = _amount; + + emit Approval(msg.sender, _spender, _id, _amount); + + return true; + } + + /** + * @notice Exports the function selectors of the ERC6909ApproveFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC6909ApproveFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.approve.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC6909/Approve/ERC6909ApproveMod.sol b/cli/project-template/src/templates/token/ERC6909/Approve/ERC6909ApproveMod.sol new file mode 100644 index 00000000..bfa14282 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Approve/ERC6909ApproveMod.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the spender address is invalid. + */ +error ERC6909InvalidSpender(address _spender); + +/** + * @notice Emitted when an approval occurs. + */ +event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _amount); +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + +/** + * @custom:storage-location erc8042:erc6909 + */ +struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; +} + +/** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ +function getStorage() pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Approves an amount of an id to a spender. + * @param _spender The address of the spender. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the approval succeeded. + */ +function approve(address _spender, uint256 _id, uint256 _amount) returns (bool) { + if (_spender == address(0)) { + revert ERC6909InvalidSpender(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + s.allowance[msg.sender][_spender][_id] = _amount; + + emit Approval(msg.sender, _spender, _id, _amount); + + return true; +} diff --git a/cli/project-template/src/templates/token/ERC6909/Burn/ERC6909BurnFacet.sol b/cli/project-template/src/templates/token/ERC6909/Burn/ERC6909BurnFacet.sol new file mode 100644 index 00000000..2edb4ac4 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Burn/ERC6909BurnFacet.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC6909BurnFacet { + /** + * @notice Thrown when the sender has insufficient balance. + */ + error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); + + /** + * @notice Thrown when the spender has insufficient allowance. + */ + error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); + + /** + * @notice Thrown when the sender address is invalid. + */ + error ERC6909InvalidSender(address _sender); + + /** + * @notice Emitted when a transfer occurs. + */ + + event Transfer( + address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount + ); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + + /** + * @custom:storage-location erc8042:erc6909 + */ + struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; + } + + /** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ + function getStorage() internal pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Burns (destroys) a specific amount of tokens from the caller's balance. + * @dev Emits a {Transfer} event to the zero address. + * @param _amount The amount of tokens to burn. + */ + function burn(uint256 _id, uint256 _amount) external { + ERC6909Storage storage s = getStorage(); + + uint256 fromBalance = s.balanceOf[msg.sender][_id]; + + if (fromBalance < _amount) { + revert ERC6909InsufficientBalance(msg.sender, fromBalance, _amount, _id); + } + + unchecked { + s.balanceOf[msg.sender][_id] = fromBalance - _amount; + } + + emit Transfer(msg.sender, msg.sender, address(0), _id, _amount); + } + + /** + * @notice Burns tokens from another account, deducting from the caller's allowance. + * @dev Emits a {Transfer} event to the zero address. + * @param _from The address whose tokens will be burned. + * @param _amount The amount of tokens to burn. + */ + function burnFrom(address _from, uint256 _id, uint256 _amount) external { + if (_from == address(0)) { + revert ERC6909InvalidSender(_from); + } + + ERC6909Storage storage s = getStorage(); + + uint256 currentAllowance = s.allowance[_from][msg.sender][_id]; + + if (msg.sender != _from && currentAllowance < type(uint256).max) { + if (currentAllowance < _amount) { + revert ERC6909InsufficientAllowance(msg.sender, currentAllowance, _amount, _id); + } + unchecked { + s.allowance[_from][msg.sender][_id] = currentAllowance - _amount; + } + } + uint256 fromBalance = s.balanceOf[_from][_id]; + if (fromBalance < _amount) { + revert ERC6909InsufficientBalance(_from, fromBalance, _amount, _id); + } + unchecked { + s.balanceOf[_from][_id] = fromBalance - _amount; + } + + emit Transfer(msg.sender, _from, address(0), _id, _amount); + } + + /** + * @notice Exports the function selectors of the ERC6909BurnFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC6909BurnFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.burn.selector, this.burnFrom.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC6909/Burn/ERC6909BurnMod.sol b/cli/project-template/src/templates/token/ERC6909/Burn/ERC6909BurnMod.sol new file mode 100644 index 00000000..00ecde94 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Burn/ERC6909BurnMod.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ +/** + * @notice Thrown when the sender has insufficient balance. + */ +error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); + +/** + * @notice Thrown when the spender has insufficient allowance. + */ +error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); + +/** + * @notice Thrown when the sender address is invalid. + */ +error ERC6909InvalidSender(address _sender); + +/** + * @notice Emitted when a transfer occurs. + */ +event Transfer( + address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount +); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + +/** + * @custom:storage-location erc8042:erc6909 + */ +struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; +} + +/** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ +function getStorage() pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Burns (destroys) a specific amount of tokens from the caller's balance. + * @dev Emits a {Transfer} event to the zero address. + * @param _amount The amount of tokens to burn. + */ +function burn(uint256 _id, uint256 _amount) { + ERC6909Storage storage s = getStorage(); + + uint256 fromBalance = s.balanceOf[msg.sender][_id]; + + if (fromBalance < _amount) { + revert ERC6909InsufficientBalance(msg.sender, fromBalance, _amount, _id); + } + + unchecked { + s.balanceOf[msg.sender][_id] = fromBalance - _amount; + } + + emit Transfer(msg.sender, msg.sender, address(0), _id, _amount); +} + +/** + * @notice Burns tokens from another account, deducting from the caller's allowance. + * @dev Emits a {Transfer} event to the zero address. + * @param _from The address whose tokens will be burned. + * @param _amount The amount of tokens to burn. + */ +function burnFrom(address _from, uint256 _id, uint256 _amount) { + if (_from == address(0)) { + revert ERC6909InvalidSender(_from); + } + + ERC6909Storage storage s = getStorage(); + + uint256 currentAllowance = s.allowance[_from][msg.sender][_id]; + + if (msg.sender != _from && currentAllowance < type(uint256).max) { + if (currentAllowance < _amount) { + revert ERC6909InsufficientAllowance(msg.sender, currentAllowance, _amount, _id); + } + unchecked { + s.allowance[_from][msg.sender][_id] = currentAllowance - _amount; + } + } + uint256 fromBalance = s.balanceOf[_from][_id]; + if (fromBalance < _amount) { + revert ERC6909InsufficientBalance(_from, fromBalance, _amount, _id); + } + unchecked { + s.balanceOf[_from][_id] = fromBalance - _amount; + } + + emit Transfer(msg.sender, _from, address(0), _id, _amount); +} diff --git a/cli/project-template/src/templates/token/ERC6909/Data/ERC6909DataFacet.sol b/cli/project-template/src/templates/token/ERC6909/Data/ERC6909DataFacet.sol new file mode 100644 index 00000000..6cdd6252 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Data/ERC6909DataFacet.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC6909DataFacet { + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + + /** + * @custom:storage-location erc8042:erc6909 + */ + struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; + } + + /** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ + function getStorage() internal pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Owner balance of an id. + * @param _owner The address of the owner. + * @param _id The id of the token. + * @return The balance of the token. + */ + function balanceOf(address _owner, uint256 _id) external view returns (uint256) { + return getStorage().balanceOf[_owner][_id]; + } + + /** + * @notice Spender allowance of an id. + * @param _owner The address of the owner. + * @param _spender The address of the spender. + * @param _id The id of the token. + * @return The allowance of the token. + */ + function allowance(address _owner, address _spender, uint256 _id) external view returns (uint256) { + return getStorage().allowance[_owner][_spender][_id]; + } + + /** + * @notice Checks if a spender is approved by an owner as an operator. + * @param _owner The address of the owner. + * @param _spender The address of the spender. + * @return The approval status. + */ + function isOperator(address _owner, address _spender) external view returns (bool) { + return getStorage().isOperator[_owner][_spender]; + } + + /** + * @notice Exports the function selectors of the ERC6909DataFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC6909DataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.balanceOf.selector, this.allowance.selector, this.isOperator.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC6909/Mint/ERC6909MintMod.sol b/cli/project-template/src/templates/token/ERC6909/Mint/ERC6909MintMod.sol new file mode 100644 index 00000000..728479b8 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Mint/ERC6909MintMod.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the receiver address is invalid. + */ +error ERC6909InvalidReceiver(address _receiver); +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + +/** + * @notice Emitted when a transfer occurs. + */ +event Transfer( + address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount +); + +bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + +/** + * @custom:storage-location erc8042:erc6909 + */ +struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; +} + +/** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ +function getStorage() pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Mints new tokens to a specified address. + * @dev Increases both total supply and the recipient's balance. + * @param _account The address receiving the newly minted tokens. + * @param _value The number of tokens to mint. + */ +function mint(address _account, uint256 _id, uint256 _value) { + if (_account == address(0)) { + revert ERC6909InvalidReceiver(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + s.balanceOf[_account][_id] += _value; + emit Transfer(msg.sender, address(0), _account, _id, _value); +} + diff --git a/cli/project-template/src/templates/token/ERC6909/Operator/ERC6909OperatorFacet.sol b/cli/project-template/src/templates/token/ERC6909/Operator/ERC6909OperatorFacet.sol new file mode 100644 index 00000000..736ad27e --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Operator/ERC6909OperatorFacet.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-6909 Minimal Multi-Token Interface + * @notice A complete, dependency-free ERC-6909 implementation using the diamond storage pattern. + */ +contract ERC6909OperatorFacet { + /** + * @notice Thrown when the spender address is invalid. + */ + error ERC6909InvalidSpender(address _spender); + + /** + * @notice Emitted when an operator is set. + */ + event OperatorSet(address indexed _owner, address indexed _spender, bool _approved); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + + /** + * @custom:storage-location erc8042:erc6909 + */ + struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; + } + + /** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ + function getStorage() internal pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Sets or removes a spender as an operator for the caller. + * @param _spender The address of the spender. + * @param _approved The approval status. + * @return Whether the operator update succeeded. + */ + function setOperator(address _spender, bool _approved) external returns (bool) { + if (_spender == address(0)) { + revert ERC6909InvalidSpender(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + s.isOperator[msg.sender][_spender] = _approved; + + emit OperatorSet(msg.sender, _spender, _approved); + + return true; + } + + /** + * @notice Exports the function selectors of the ERC6909OPeratorFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC6909OPeratorFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.setOperator.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC6909/Operator/ERC6909OperatorMod.sol b/cli/project-template/src/templates/token/ERC6909/Operator/ERC6909OperatorMod.sol new file mode 100644 index 00000000..eb237150 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Operator/ERC6909OperatorMod.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-6909 Minimal Multi-Token Interface + * @notice A complete, dependency-free ERC-6909 implementation using the diamond storage pattern. + */ +/** + * @notice Thrown when the spender address is invalid. + */ +error ERC6909InvalidSpender(address _spender); + +/** + * @notice Emitted when an operator is set. + */ +event OperatorSet(address indexed _owner, address indexed _spender, bool _approved); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + +/** + * @custom:storage-location erc8042:erc6909 + */ +struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; +} + +/** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ +function getStorage() pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Sets or removes a spender as an operator for the caller. + * @param _spender The address of the spender. + * @param _approved The approval status. + * @return Whether the operator update succeeded. + */ +function setOperator(address _spender, bool _approved) returns (bool) { + if (_spender == address(0)) { + revert ERC6909InvalidSpender(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + s.isOperator[msg.sender][_spender] = _approved; + + emit OperatorSet(msg.sender, _spender, _approved); + + return true; +} + diff --git a/cli/project-template/src/templates/token/ERC6909/Transfer/ERC6909TransferFacet.sol b/cli/project-template/src/templates/token/ERC6909/Transfer/ERC6909TransferFacet.sol new file mode 100644 index 00000000..1e3ce474 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Transfer/ERC6909TransferFacet.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC6909TransferFacet { + /** + * @notice Thrown when the sender has insufficient balance. + */ + error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); + + /** + * @notice Thrown when the spender has insufficient allowance. + */ + error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); + + /** + * @notice Thrown when the receiver address is invalid. + */ + error ERC6909InvalidReceiver(address _receiver); + + /** + * @notice Thrown when the sender address is invalid. + */ + error ERC6909InvalidSender(address _sender); + /** + * @notice Emitted when a transfer occurs. + */ + event Transfer( + address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount + ); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + + /** + * @custom:storage-location erc8042:erc6909 + */ + struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; + } + + /** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ + function getStorage() internal pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Transfers an amount of an id from the caller to a receiver. + * @param _receiver The address of the receiver. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the transfer succeeded. + */ + function transfer(address _receiver, uint256 _id, uint256 _amount) external returns (bool) { + if (_receiver == address(0)) { + revert ERC6909InvalidReceiver(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + uint256 fromBalance = s.balanceOf[msg.sender][_id]; + + if (fromBalance < _amount) { + revert ERC6909InsufficientBalance(msg.sender, fromBalance, _amount, _id); + } + + unchecked { + s.balanceOf[msg.sender][_id] = fromBalance - _amount; + } + + s.balanceOf[_receiver][_id] += _amount; + + emit Transfer(msg.sender, msg.sender, _receiver, _id, _amount); + + return true; + } + + /** + * @notice Transfers an amount of an id from a sender to a receiver. + * @param _sender The address of the sender. + * @param _receiver The address of the receiver. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the transfer succeeded. + */ + function transferFrom(address _sender, address _receiver, uint256 _id, uint256 _amount) external returns (bool) { + if (_sender == address(0)) { + revert ERC6909InvalidSender(address(0)); + } + + if (_receiver == address(0)) { + revert ERC6909InvalidReceiver(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + if (msg.sender != _sender && !s.isOperator[_sender][msg.sender]) { + uint256 currentAllowance = s.allowance[_sender][msg.sender][_id]; + if (currentAllowance < type(uint256).max) { + if (currentAllowance < _amount) { + revert ERC6909InsufficientAllowance(msg.sender, currentAllowance, _amount, _id); + } + unchecked { + s.allowance[_sender][msg.sender][_id] = currentAllowance - _amount; + } + } + } + + uint256 fromBalance = s.balanceOf[_sender][_id]; + if (fromBalance < _amount) { + revert ERC6909InsufficientBalance(_sender, fromBalance, _amount, _id); + } + unchecked { + s.balanceOf[_sender][_id] = fromBalance - _amount; + } + + s.balanceOf[_receiver][_id] += _amount; + + emit Transfer(msg.sender, _sender, _receiver, _id, _amount); + + return true; + } + + /** + * @notice Exports the function selectors of the ERC6909TransferFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC6909TransferFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.transfer.selector, this.transferFrom.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC6909/Transfer/ERC6909TransferMod.sol b/cli/project-template/src/templates/token/ERC6909/Transfer/ERC6909TransferMod.sol new file mode 100644 index 00000000..d2a1c2e6 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC6909/Transfer/ERC6909TransferMod.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC6909ransferMod + * @notice Provides transfer internal functions for ERC-6909 tokens. + * @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions. + */ + +/** + * @notice Thrown when the sender has insufficient balance. + */ +error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); + +/** + * @notice Thrown when the spender has insufficient allowance. + */ +error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); + +/** + * @notice Thrown when the receiver address is invalid. + */ +error ERC6909InvalidReceiver(address _receiver); + +/** + * @notice Thrown when the sender address is invalid. + */ +error ERC6909InvalidSender(address _sender); +/** + * @notice Emitted when a transfer occurs. + */ +event Transfer( + address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount +); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc6909"); + +/** + * @custom:storage-location erc8042:erc6909 + */ +struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; +} + +/** + * @notice Returns a pointer to the ERC-6909 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC6909Storage struct in storage. + */ +function getStorage() pure returns (ERC6909Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Transfers an amount of an id from the caller to a receiver. + * @param _receiver The address of the receiver. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the transfer succeeded. + */ +function transfer(address _receiver, uint256 _id, uint256 _amount) returns (bool) { + if (_receiver == address(0)) { + revert ERC6909InvalidReceiver(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + uint256 fromBalance = s.balanceOf[msg.sender][_id]; + + if (fromBalance < _amount) { + revert ERC6909InsufficientBalance(msg.sender, fromBalance, _amount, _id); + } + + unchecked { + s.balanceOf[msg.sender][_id] = fromBalance - _amount; + } + + s.balanceOf[_receiver][_id] += _amount; + + emit Transfer(msg.sender, msg.sender, _receiver, _id, _amount); + + return true; +} + +/** + * @notice Transfers an amount of an id from a sender to a receiver. + * @param _sender The address of the sender. + * @param _receiver The address of the receiver. + * @param _id The id of the token. + * @param _amount The amount of the token. + * @return Whether the transfer succeeded. + */ +function transferFrom(address _sender, address _receiver, uint256 _id, uint256 _amount) returns (bool) { + if (_sender == address(0)) { + revert ERC6909InvalidSender(address(0)); + } + + if (_receiver == address(0)) { + revert ERC6909InvalidReceiver(address(0)); + } + + ERC6909Storage storage s = getStorage(); + + if (msg.sender != _sender && !s.isOperator[_sender][msg.sender]) { + uint256 currentAllowance = s.allowance[_sender][msg.sender][_id]; + if (currentAllowance < type(uint256).max) { + if (currentAllowance < _amount) { + revert ERC6909InsufficientAllowance(msg.sender, currentAllowance, _amount, _id); + } + unchecked { + s.allowance[_sender][msg.sender][_id] = currentAllowance - _amount; + } + } + } + + uint256 fromBalance = s.balanceOf[_sender][_id]; + if (fromBalance < _amount) { + revert ERC6909InsufficientBalance(_sender, fromBalance, _amount, _id); + } + unchecked { + s.balanceOf[_sender][_id] = fromBalance - _amount; + } + + s.balanceOf[_receiver][_id] += _amount; + + emit Transfer(msg.sender, _sender, _receiver, _id, _amount); + + return true; +} diff --git a/cli/project-template/src/templates/token/ERC721/Approve/ERC721ApproveFacet.sol b/cli/project-template/src/templates/token/ERC721/Approve/ERC721ApproveFacet.sol new file mode 100644 index 00000000..a9c79d4c --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Approve/ERC721ApproveFacet.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Approve + */ +contract ERC721ApproveFacet { + /** + * @notice Error indicating that the queried token does not exist. + */ + error ERC721NonexistentToken(uint256 _tokenId); + + /** + * @notice Error indicating the approver address is invalid. + */ + error ERC721InvalidApprover(address _approver); + + /** + * @notice Error indicating the operator address is invalid. + */ + error ERC721InvalidOperator(address _operator); + + /** + * @notice Emitted when the approved address for an NFT is changed or reaffirmed. + */ + event Approval(address indexed _owner, address indexed _to, uint256 indexed _tokenId); + + /** + * @notice Emitted when an operator is enabled or disabled for an owner. + */ + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + bytes32 constant STORAGE_POSITION = keccak256("erc721"); + + /** + * @custom:storage-location erc8042:erc721 + */ + struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getStorage() internal pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Approves another address to transfer the given token ID. + * @param _to The address to be approved. + * @param _tokenId The token ID to approve. + */ + function approve(address _to, uint256 _tokenId) external { + ERC721Storage storage s = getStorage(); + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (msg.sender != owner && !s.isApprovedForAll[owner][msg.sender]) { + revert ERC721InvalidApprover(msg.sender); + } + s.approved[_tokenId] = _to; + emit Approval(owner, _to, _tokenId); + } + + /** + * @notice Approves or revokes permission for an operator to manage all caller's assets. + * @param _operator The operator address to set approval for. + * @param _approved True to approve, false to revoke. + */ + function setApprovalForAll(address _operator, bool _approved) external { + if (_operator == address(0)) { + revert ERC721InvalidOperator(_operator); + } + getStorage().isApprovedForAll[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + /** + * @notice Exports the function selectors of the ERC721ApproveFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC721ApproveFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.approve.selector, this.setApprovalForAll.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC721/Approve/ERC721ApproveMod.sol b/cli/project-template/src/templates/token/ERC721/Approve/ERC721ApproveMod.sol new file mode 100644 index 00000000..012eff79 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Approve/ERC721ApproveMod.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Approve Module + */ + +/** + * @notice Error indicating that the queried token does not exist. + */ +error ERC721NonexistentToken(uint256 _tokenId); +/** + * @notice Error indicating the operator address is invalid. + */ +error ERC721InvalidOperator(address _operator); + +/** + * @notice Emitted when the approved address for an NFT is changed or reaffirmed. + */ +event Approval(address indexed _owner, address indexed _to, uint256 indexed _tokenId); + +/** + * @notice Emitted when an operator is enabled or disabled for an owner. + */ +event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + +bytes32 constant STORAGE_POSITION = keccak256("erc721"); + +/** + * @custom:storage-location erc8042:erc721 + */ +struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +} + +/** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ +function getStorage() pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Approves another address to transfer the given token ID. + * @param _to The address to be approved. + * @param _tokenId The token ID to approve. + */ +function approve(address _to, uint256 _tokenId) { + ERC721Storage storage s = getStorage(); + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + s.approved[_tokenId] = _to; + emit Approval(owner, _to, _tokenId); +} + +/** + * @notice Approves or revokes permission for an operator to manage all users's assets. + * @param _operator The operator address to set approval for. + * @param _approved True to approve, false to revoke. + */ +function setApprovalForAll(address user, address _operator, bool _approved) { + if (_operator == address(0)) { + revert ERC721InvalidOperator(_operator); + } + getStorage().isApprovedForAll[user][_operator] = _approved; + emit ApprovalForAll(user, _operator, _approved); +} + diff --git a/cli/project-template/src/templates/token/ERC721/Burn/ERC721BurnFacet.sol b/cli/project-template/src/templates/token/ERC721/Burn/ERC721BurnFacet.sol new file mode 100644 index 00000000..1a9cefb6 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Burn/ERC721BurnFacet.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Token + * @notice A complete, dependency-free ERC-721 implementation using the diamond storage pattern. + * @dev This facet provides metadata, ownership, approvals, safe transfers, minting, burning, and helpers. + */ +contract ERC721BurnFacet { + /** + * @notice Error indicating that the queried token does not exist. + */ + error ERC721NonexistentToken(uint256 _tokenId); + + /** + * @notice Error indicating the operator lacks approval to transfer the given token. + */ + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + /** + * @notice Emitted when ownership of an NFT changes by any mechanism. + */ + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + bytes32 constant STORAGE_POSITION = keccak256("erc721"); + + /** + * @custom:storage-location erc8042:erc721 + */ + struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getStorage() internal pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Burns (destroys) a token, removing it from enumeration tracking. + * @param _tokenId The ID of the token to burn. + */ + function burn(uint256 _tokenId) external { + ERC721Storage storage s = getStorage(); + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (msg.sender != owner) { + if (!s.isApprovedForAll[owner][msg.sender] && msg.sender != s.approved[_tokenId]) { + revert ERC721InsufficientApproval(msg.sender, _tokenId); + } + } + unchecked { + s.balanceOf[owner]--; + } + delete s.ownerOf[_tokenId]; + delete s.approved[_tokenId]; + emit Transfer(owner, address(0), _tokenId); + } + + /** + * @notice Burns (destroys) a token, removing it from enumeration tracking. + * @param _tokenIds The ID of the token to burn. + */ + function burnBatch(uint256[] memory _tokenIds) external { + ERC721Storage storage s = getStorage(); + for (uint256 i; i < _tokenIds.length; i++) { + uint256 tokenId = _tokenIds[i]; + address owner = s.ownerOf[tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } + if (msg.sender != owner) { + if (!s.isApprovedForAll[owner][msg.sender] && msg.sender != s.approved[tokenId]) { + revert ERC721InsufficientApproval(msg.sender, tokenId); + } + } + unchecked { + s.balanceOf[owner]--; + } + delete s.ownerOf[tokenId]; + delete s.approved[tokenId]; + emit Transfer(owner, address(0), tokenId); + } + } + + /** + * @notice Exports the function selectors of the ERC721BurnFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC721BurnFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.burn.selector, this.burnBatch.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC721/Burn/ERC721BurnMod.sol b/cli/project-template/src/templates/token/ERC721/Burn/ERC721BurnMod.sol new file mode 100644 index 00000000..d84481ae --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Burn/ERC721BurnMod.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when attempting to interact with a non-existent token. + * @param _tokenId The ID of the token that does not exist. + */ +error ERC721NonexistentToken(uint256 _tokenId); + +/** + * @notice Emitted when ownership of a token changes, including minting and burning. + * @param _from The address transferring the token, or zero for minting. + * @param _to The address receiving the token, or zero for burning. + * @param _tokenId The ID of the token being transferred. + */ +event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + +/** + * @dev Storage position constant defined via keccak256 hash of diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc721"); + +/** + * @notice Storage layout for ERC-721 token management. + * @dev Defines ownership, balances, approvals, and operator mappings per ERC-721 standard. + * @custom:storage-location erc8042:erc721 + */ +struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +} + +/** + * @notice Returns the ERC-721 storage struct from its predefined slot. + * @dev Uses inline assembly to access diamond storage location. + * @return s The storage reference for ERC-721 state variables. + */ +function getStorage() pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Burns (destroys) a specific ERC-721 token. + * @dev Reverts if the token does not exist. Clears ownership and approval. + * This module does not perform approval checks. Ensure proper ownership or approval validation before calling this function. + * @param _tokenId The ID of the token to burn. + */ +function burn(uint256 _tokenId) { + ERC721Storage storage s = getStorage(); + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + delete s.ownerOf[_tokenId]; + delete s.approved[_tokenId]; + unchecked { + s.balanceOf[owner]--; + } + emit Transfer(owner, address(0), _tokenId); +} diff --git a/cli/project-template/src/templates/token/ERC721/Data/ERC721DataFacet.sol b/cli/project-template/src/templates/token/ERC721/Data/ERC721DataFacet.sol new file mode 100644 index 00000000..b8d81aad --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Data/ERC721DataFacet.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Data Facet + */ +contract ERC721DataFacet { + /** + * @notice Error indicating the queried owner address is invalid (zero address). + */ + error ERC721InvalidOwner(address _owner); + + /** + * @notice Error indicating that the queried token does not exist. + */ + error ERC721NonexistentToken(uint256 _tokenId); + + bytes32 constant STORAGE_POSITION = keccak256("erc721"); + + /** + * @custom:storage-location erc8042:erc721 + */ + struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getStorage() internal pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the number of tokens owned by a given address. + * @param _owner The address to query the balance of. + * @return The balance (number of tokens) owned by `_owner`. + */ + function balanceOf(address _owner) external view returns (uint256) { + if (_owner == address(0)) { + revert ERC721InvalidOwner(_owner); + } + return getStorage().balanceOf[_owner]; + } + + /** + * @notice Returns the owner of a given token ID. + * @param _tokenId The token ID to query. + * @return The address of the token owner. + */ + function ownerOf(uint256 _tokenId) public view returns (address) { + address owner = getStorage().ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + return owner; + } + + /** + * @notice Returns the approved address for a given token ID. + * @param _tokenId The token ID to query the approval of. + * @return The approved address for the token. + */ + function getApproved(uint256 _tokenId) external view returns (address) { + address owner = getStorage().ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + return getStorage().approved[_tokenId]; + } + + /** + * @notice Returns true if an operator is approved to manage all of an owner's assets. + * @param _owner The token owner. + * @param _operator The operator address. + * @return True if the operator is approved for all tokens of the owner. + */ + function isApprovedForAll(address _owner, address _operator) external view returns (bool) { + return getStorage().isApprovedForAll[_owner][_operator]; + } + + /** + * @notice Exports the function selectors of the ERC721DataFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC721DataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat( + this.balanceOf.selector, this.ownerOf.selector, this.getApproved.selector, this.isApprovedForAll.selector + ); + } +} diff --git a/cli/project-template/src/templates/token/ERC721/Enumerable/Burn/ERC721EnumerableBurnFacet.sol b/cli/project-template/src/templates/token/ERC721/Enumerable/Burn/ERC721EnumerableBurnFacet.sol new file mode 100644 index 00000000..c5d99eea --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Enumerable/Burn/ERC721EnumerableBurnFacet.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-721 Enumerable Token + * @notice A complete, dependency-free ERC-721 implementation with enumeration support using a custom storage layout. + * @dev Provides metadata, ownership, approvals, enumeration, safe transfers, minting, and burning features. + */ +contract ERC721EnumerableBurnFacet { + /** + * @notice Thrown when operating on a non-existent token. + */ + error ERC721NonexistentToken(uint256 _tokenId); + /** + * @notice Thrown when the operator lacks sufficient approval for a transfer. + */ + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + /** + * @notice Emitted when a token is transferred between addresses. + */ + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + bytes32 constant STORAGE_POSITION = keccak256("erc721.enumerable"); + + /** + * @custom:storage-location erc8042:erc721.enumerable + */ + struct ERC721EnumerableStorage { + mapping(address owner => mapping(uint256 index => uint256 tokenId)) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndex; + } + + /** + * @notice Returns the storage struct used by this facet. + * @return s The ERC721Enumerable storage struct. + */ + function getStorage() internal pure returns (ERC721EnumerableStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + bytes32 constant ERC721_STORAGE_POSITION = keccak256("erc721"); + + /** + * @custom:storage-location erc8042:erc721 + */ + struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getERC721Storage() internal pure returns (ERC721Storage storage s) { + bytes32 position = ERC721_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Burns (destroys) a token, removing it from enumeration tracking. + * @param _tokenId The ID of the token to burn. + */ + function burn(uint256 _tokenId) external { + ERC721EnumerableStorage storage s = getStorage(); + ERC721Storage storage erc721Storage = getERC721Storage(); + address owner = erc721Storage.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + + if (msg.sender != owner) { + if (!erc721Storage.isApprovedForAll[owner][msg.sender] && msg.sender != erc721Storage.approved[_tokenId]) { + revert ERC721InsufficientApproval(msg.sender, _tokenId); + } + } + + delete erc721Storage.ownerOf[_tokenId]; + delete erc721Storage.approved[_tokenId]; + + unchecked { + /** + * Remove from owner's list + */ + uint256 tokenIndex = s.ownerTokensIndex[_tokenId]; + uint256 lastTokenIndex = erc721Storage.balanceOf[owner] - 1; + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.ownerTokens[owner][lastTokenIndex]; + s.ownerTokens[owner][tokenIndex] = lastTokenId; + s.ownerTokensIndex[lastTokenId] = tokenIndex; + } + erc721Storage.balanceOf[owner]--; + + /** + * Remove from all tokens list + */ + tokenIndex = s.allTokensIndex[_tokenId]; + lastTokenIndex = s.allTokens.length - 1; + + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.allTokens[lastTokenIndex]; + s.allTokens[tokenIndex] = lastTokenId; + s.allTokensIndex[lastTokenId] = tokenIndex; + } + s.allTokens.pop(); + } + + emit Transfer(owner, address(0), _tokenId); + } + + /** + * @notice Exports the function selectors of the ERC721EnumerableBurnFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC721EnumerableBurnFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.burn.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC721/Enumerable/Burn/ERC721EnumerableBurnMod.sol b/cli/project-template/src/templates/token/ERC721/Enumerable/Burn/ERC721EnumerableBurnMod.sol new file mode 100644 index 00000000..8857c978 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Enumerable/Burn/ERC721EnumerableBurnMod.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when operating on a non-existent token. + */ +error ERC721NonexistentToken(uint256 _tokenId); + +/** + * @notice Emitted when a token is transferred between addresses. + */ +event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + +bytes32 constant STORAGE_POSITION = keccak256("erc721.enumerable"); + +/** + * @custom:storage-location erc8042:erc721.enumerable + */ +struct ERC721EnumerableStorage { + mapping(address owner => mapping(uint256 index => uint256 tokenId)) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndex; +} + +/** + * @notice Returns the storage struct used by this facet. + * @return s The ERC721Enumerable storage struct. + */ +function getStorage() pure returns (ERC721EnumerableStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +bytes32 constant ERC721_STORAGE_POSITION = keccak256("erc721"); + +/** + * @custom:storage-location erc8042:erc721 + */ +struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +} + +/** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ +function getERC721Storage() pure returns (ERC721Storage storage s) { + bytes32 position = ERC721_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Burns (destroys) a token, removing it from enumeration tracking. + * @dev This module does not check for approval. Use the facet for approval-checked burns. + * @param _tokenId The ID of the token to burn. + */ +function burn(uint256 _tokenId) { + ERC721EnumerableStorage storage s = getStorage(); + ERC721Storage storage erc721Storage = getERC721Storage(); + address owner = erc721Storage.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + + delete erc721Storage.ownerOf[_tokenId]; + delete erc721Storage.approved[_tokenId]; + + unchecked { + /** + * Remove from owner's list + */ + uint256 tokenIndex = s.ownerTokensIndex[_tokenId]; + uint256 lastTokenIndex = erc721Storage.balanceOf[owner] - 1; + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.ownerTokens[owner][lastTokenIndex]; + s.ownerTokens[owner][tokenIndex] = lastTokenId; + s.ownerTokensIndex[lastTokenId] = tokenIndex; + } + erc721Storage.balanceOf[owner]--; + + /** + * Remove from all tokens list + */ + tokenIndex = s.allTokensIndex[_tokenId]; + lastTokenIndex = s.allTokens.length - 1; + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.allTokens[lastTokenIndex]; + s.allTokens[tokenIndex] = lastTokenId; + s.allTokensIndex[lastTokenId] = tokenIndex; + } + s.allTokens.pop(); + } + + emit Transfer(owner, address(0), _tokenId); +} + diff --git a/cli/project-template/src/templates/token/ERC721/Enumerable/Data/ERC721EnumerableDataFacet.sol b/cli/project-template/src/templates/token/ERC721/Enumerable/Data/ERC721EnumerableDataFacet.sol new file mode 100644 index 00000000..8aa2ba58 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Enumerable/Data/ERC721EnumerableDataFacet.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +contract ERC721EnumerableDataFacet { + /** + * @notice Thrown when an index is out of bounds during enumeration. + */ + error ERC721OutOfBoundsIndex(address _owner, uint256 _index); + + bytes32 constant STORAGE_POSITION = keccak256("erc721.enumerable"); + + /** + * @custom:storage-location erc8042:erc721.enumerable + */ + struct ERC721EnumerableStorage { + mapping(address owner => mapping(uint256 index => uint256 tokenId)) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + } + + /** + * @notice Returns the storage struct used by this facet. + * @return s The ERC721Enumerable storage struct. + */ + function getStorage() internal pure returns (ERC721EnumerableStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + bytes32 constant ERC721_STORAGE_POSITION = keccak256("erc721"); + + /** + * @custom:storage-location erc8042:erc721 + */ + struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getERC721Storage() internal pure returns (ERC721Storage storage s) { + bytes32 position = ERC721_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the total number of tokens in existence. + * @return The total supply of tokens. + */ + function totalSupply() external view returns (uint256) { + return getStorage().allTokens.length; + } + + /** + * @notice Returns a token ID owned by a given address at a specific index. + * @param _owner The address to query. + * @param _index The index of the token. + * @return The token ID owned by `_owner` at `_index`. + */ + function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256) { + ERC721EnumerableStorage storage s = getStorage(); + ERC721Storage storage erc721Storage = getERC721Storage(); + if (_index >= erc721Storage.balanceOf[_owner]) { + revert ERC721OutOfBoundsIndex(_owner, _index); + } + return s.ownerTokens[_owner][_index]; + } + + /** + * @notice Enumerate valid NFTs + * @dev Throws if `_index` >= `totalSupply()`. + * @param _index A counter less than `totalSupply()` + * @return The token identifier for the `_index`th NFT, + */ + function tokenByIndex(uint256 _index) external view returns (uint256) { + ERC721EnumerableStorage storage s = getStorage(); + if (_index >= s.allTokens.length) { + revert ERC721OutOfBoundsIndex(address(0), _index); + } + return s.allTokens[_index]; + } + + /** + * @notice Exports the function selectors of the ERC721EnumerableDataFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC721EnumerableDataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.totalSupply.selector, this.tokenOfOwnerByIndex.selector, this.tokenByIndex.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC721/Enumerable/Mint/ERC721EnumerableMintMod.sol b/cli/project-template/src/templates/token/ERC721/Enumerable/Mint/ERC721EnumerableMintMod.sol new file mode 100644 index 00000000..28e74f9f --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Enumerable/Mint/ERC721EnumerableMintMod.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the sender address is invalid. + * @param _sender The invalid sender address. + */ +error ERC721InvalidSender(address _sender); + +/** + * @notice Thrown when the receiver address is invalid. + * @param _receiver The invalid receiver address. + */ +error ERC721InvalidReceiver(address _receiver); + +/** + * @notice Emitted when a token is transferred between addresses. + */ +event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + +bytes32 constant STORAGE_POSITION = keccak256("erc721.enumerable"); + +/** + * @custom:storage-location erc8042:erc721.enumerable + */ +struct ERC721EnumerableStorage { + mapping(address owner => mapping(uint256 index => uint256 tokenId)) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndex; +} + +/** + * @notice Returns the storage struct used by this facet. + * @return s The ERC721Enumerable storage struct. + */ +function getStorage() pure returns (ERC721EnumerableStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +bytes32 constant ERC721_STORAGE_POSITION = keccak256("erc721"); + +/** + * @custom:storage-location erc8042:erc721 + */ +struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +} + +/** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ +function getERC721Storage() pure returns (ERC721Storage storage s) { + bytes32 position = ERC721_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Mints a new ERC-721 token to the specified address, adding it to enumeration lists. + * @dev Reverts if the receiver address is zero or if the token already exists. + * @param _to The address that will own the newly minted token. + * @param _tokenId The ID of the token to mint. + */ +function mint(address _to, uint256 _tokenId) { + ERC721EnumerableStorage storage s = getStorage(); + ERC721Storage storage erc721Storage = getERC721Storage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + if (erc721Storage.ownerOf[_tokenId] != address(0)) { + revert ERC721InvalidSender(address(0)); + } + + erc721Storage.ownerOf[_tokenId] = _to; + uint256 tokenIndex = erc721Storage.balanceOf[_to]; + s.ownerTokensIndex[_tokenId] = tokenIndex; + s.ownerTokens[_to][tokenIndex] = _tokenId; + unchecked { + erc721Storage.balanceOf[_to] = tokenIndex + 1; + } + s.allTokensIndex[_tokenId] = s.allTokens.length; + s.allTokens.push(_tokenId); + emit Transfer(address(0), _to, _tokenId); +} + diff --git a/cli/project-template/src/templates/token/ERC721/Enumerable/Transfer/ERC721EnumerableTransferFacet.sol b/cli/project-template/src/templates/token/ERC721/Enumerable/Transfer/ERC721EnumerableTransferFacet.sol new file mode 100644 index 00000000..f2919a11 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Enumerable/Transfer/ERC721EnumerableTransferFacet.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC721 Receiver Interface + * @notice Interface for contracts that want to support safe ERC721 token transfers. + * @dev Implementers must return the function selector to confirm token receipt. + */ +interface IERC721Receiver { + /** + * @notice Handles the receipt of an NFT. + * @param _operator The address which initiated the transfer. + * @param _from The previous owner of the token. + * @param _tokenId The NFT identifier being transferred. + * @param _data Additional data with no specified format. + * @return A bytes4 value indicating acceptance of the transfer. + */ + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) + external + returns (bytes4); +} + +/** + * @title ERC-721 Enumerable Token + * @notice A complete, dependency-free ERC-721 implementation with enumeration support using a custom storage layout. + * @dev Provides metadata, ownership, approvals, enumeration, safe transfers, minting, and burning features. + */ +contract ERC721EnumerableTransferFacet { + /** + * @notice Thrown when operating on a non-existent token. + */ + error ERC721NonexistentToken(uint256 _tokenId); + /** + * @notice Thrown when the provided owner does not match the actual owner of the token. + */ + error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + /** + * @notice Thrown when the receiver address is invalid. + */ + error ERC721InvalidReceiver(address _receiver); + /** + * @notice Thrown when the operator lacks sufficient approval for a transfer. + */ + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + /** + * @notice Emitted when a token is transferred between addresses. + */ + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + bytes32 constant STORAGE_POSITION = keccak256("erc721.enumerable"); + + /** + * @custom:storage-location erc8042:erc721.enumerable + */ + struct ERC721EnumerableStorage { + mapping(address owner => mapping(uint256 index => uint256 tokenId)) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndex; + } + + /** + * @notice Returns the storage struct used by this facet. + * @return s The ERC721Enumerable storage struct. + */ + function getStorage() internal pure returns (ERC721EnumerableStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + bytes32 constant ERC721_STORAGE_POSITION = keccak256("erc721"); + + /** + * @custom:storage-location erc8042:erc721 + */ + struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getERC721Storage() internal pure returns (ERC721Storage storage s) { + bytes32 position = ERC721_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Internal function to transfer ownership of a token ID. + * @param _from The address sending the token. + * @param _to The address receiving the token. + * @param _tokenId The token ID being transferred. + */ + function internalTransferFrom(address _from, address _to, uint256 _tokenId) internal { + ERC721EnumerableStorage storage s = getStorage(); + ERC721Storage storage erc721Storage = getERC721Storage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address owner = erc721Storage.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (owner != _from) { + revert ERC721IncorrectOwner(_from, _tokenId, owner); + } + if (msg.sender != _from) { + if (!erc721Storage.isApprovedForAll[_from][msg.sender] && msg.sender != erc721Storage.approved[_tokenId]) { + revert ERC721InsufficientApproval(msg.sender, _tokenId); + } + } + delete erc721Storage.approved[_tokenId]; + unchecked { + uint256 tokenIndex = s.ownerTokensIndex[_tokenId]; + uint256 lastTokenIndex = erc721Storage.balanceOf[_from] - 1; + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.ownerTokens[_from][lastTokenIndex]; + s.ownerTokens[_from][tokenIndex] = lastTokenId; + s.ownerTokensIndex[lastTokenId] = tokenIndex; + } + erc721Storage.balanceOf[_from]--; + + tokenIndex = erc721Storage.balanceOf[_to]; + s.ownerTokensIndex[_tokenId] = tokenIndex; + s.ownerTokens[_to][tokenIndex] = _tokenId; + erc721Storage.balanceOf[_to] = tokenIndex + 1; + erc721Storage.ownerOf[_tokenId] = _to; + } + emit Transfer(_from, _to, _tokenId); + } + + /** + * @notice Transfers a token from one address to another. + * @param _from The current owner of the token. + * @param _to The recipient address. + * @param _tokenId The token ID to transfer. + */ + function transferFrom(address _from, address _to, uint256 _tokenId) external { + internalTransferFrom(_from, _to, _tokenId); + } + + /** + * @notice Safely transfers a token, checking for receiver contract compatibility. + * @param _from The current owner of the token. + * @param _to The recipient address. + * @param _tokenId The token ID to transfer. + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external { + internalTransferFrom(_from, _to, _tokenId); + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, "") returns (bytes4 returnValue) { + if (returnValue != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(_to); + } + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } + + /** + * @notice Safely transfers a token with additional data. + * @param _from The current owner of the token. + * @param _to The recipient address. + * @param _tokenId The token ID to transfer. + * @param _data Additional data to send to the receiver contract. + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external { + internalTransferFrom(_from, _to, _tokenId); + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4 returnValue) { + if (returnValue != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(_to); + } + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } + + /** + * @notice Exports the function selectors of the ERC721EnumerableTransferFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC721EnumerableTransferFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat( + this.transferFrom.selector, + bytes4(keccak256("safeTransferFrom(address,address,uint256)")), + bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)")) + ); + } +} diff --git a/cli/project-template/src/templates/token/ERC721/Enumerable/Transfer/ERC721EnumerableTransferMod.sol b/cli/project-template/src/templates/token/ERC721/Enumerable/Transfer/ERC721EnumerableTransferMod.sol new file mode 100644 index 00000000..6e5b2f5b --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Enumerable/Transfer/ERC721EnumerableTransferMod.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC721 Receiver Interface + * @notice Interface for contracts that want to support safe ERC721 token transfers. + * @dev Implementers must return the function selector to confirm token receipt. + */ +interface IERC721Receiver { + /** + * @notice Handles the receipt of an NFT. + * @param _operator The address which initiated the transfer. + * @param _from The previous owner of the token. + * @param _tokenId The NFT identifier being transferred. + * @param _data Additional data with no specified format. + * @return A bytes4 value indicating acceptance of the transfer. + */ + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) + external + returns (bytes4); +} + +/** + * @notice Thrown when operating on a non-existent token. + */ +error ERC721NonexistentToken(uint256 _tokenId); +/** + * @notice Thrown when the provided owner does not match the actual owner of the token. + */ +error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); +/** + * @notice Thrown when the receiver address is invalid. + */ +error ERC721InvalidReceiver(address _receiver); + +/** + * @notice Emitted when a token is transferred between addresses. + */ +event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + +bytes32 constant STORAGE_POSITION = keccak256("erc721.enumerable"); + +/** + * @custom:storage-location erc8042:erc721.enumerable + */ +struct ERC721EnumerableStorage { + mapping(address owner => mapping(uint256 index => uint256 tokenId)) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndex; +} + +/** + * @notice Returns the storage struct used by this facet. + * @return s The ERC721Enumerable storage struct. + */ +function getStorage() pure returns (ERC721EnumerableStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +bytes32 constant ERC721_STORAGE_POSITION = keccak256("erc721"); + +/** + * @custom:storage-location erc8042:erc721 + */ +struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +} + +/** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ +function getERC721Storage() pure returns (ERC721Storage storage s) { + bytes32 position = ERC721_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Internal function to transfer ownership of a token ID. + * @param _from The address sending the token. + * @param _to The address receiving the token. + * @param _tokenId The token ID being transferred. + */ +function transferFrom(address _from, address _to, uint256 _tokenId) { + ERC721EnumerableStorage storage s = getStorage(); + ERC721Storage storage erc721Storage = getERC721Storage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address owner = erc721Storage.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (owner != _from) { + revert ERC721IncorrectOwner(_from, _tokenId, owner); + } + delete erc721Storage.approved[_tokenId]; + + unchecked { + uint256 tokenIndex = s.ownerTokensIndex[_tokenId]; + uint256 lastTokenIndex = erc721Storage.balanceOf[_from] - 1; + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.ownerTokens[_from][lastTokenIndex]; + s.ownerTokens[_from][tokenIndex] = lastTokenId; + s.ownerTokensIndex[lastTokenId] = tokenIndex; + } + erc721Storage.balanceOf[_from]--; + + tokenIndex = erc721Storage.balanceOf[_to]; + s.ownerTokensIndex[_tokenId] = tokenIndex; + s.ownerTokens[_to][tokenIndex] = _tokenId; + erc721Storage.balanceOf[_to] = tokenIndex + 1; + erc721Storage.ownerOf[_tokenId] = _to; + } + emit Transfer(_from, _to, _tokenId); +} + +/** + * @notice Safely transfers a token, checking for receiver contract compatibility. + * @param _from The current owner of the token. + * @param _to The recipient address. + * @param _tokenId The token ID to transfer. + */ +function safeTransferFrom(address _from, address _to, uint256 _tokenId) { + transferFrom(_from, _to, _tokenId); + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, "") returns (bytes4 returnValue) { + if (returnValue != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(_to); + } + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } +} + +/** + * @notice Safely transfers a token with additional data. + * @param _from The current owner of the token. + * @param _to The recipient address. + * @param _tokenId The token ID to transfer. + * @param _data Additional data to send to the receiver contract. + */ +function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory _data) { + transferFrom(_from, _to, _tokenId); + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4 returnValue) { + if (returnValue != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(_to); + } + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } +} + diff --git a/cli/project-template/src/templates/token/ERC721/Metadata/ERC721MetadataFacet.sol b/cli/project-template/src/templates/token/ERC721/Metadata/ERC721MetadataFacet.sol new file mode 100644 index 00000000..0c79e43a --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Metadata/ERC721MetadataFacet.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/** + * @title ERC-721 Token + * @notice A complete, dependency-free ERC-721 implementation using the diamond storage pattern. + * @dev This facet provides metadata, ownership, approvals, safe transfers, minting, burning, and helpers. + */ +contract ERC721MetadataFacet { + /** + * @notice Error indicating that the queried token does not exist. + */ + error ERC721NonexistentToken(uint256 _tokenId); + + /** + * @notice Error indicating the queried owner address is invalid (zero address). + */ + error ERC721InvalidOwner(address _owner); + + bytes32 constant STORAGE_POSITION = keccak256("erc721.metadata"); + + /** + * @custom:storage-location erc8042:erc721.metadata + */ + struct ERC721MetadataStorage { + string name; + string symbol; + string baseURI; + } + + bytes32 constant ERC721_STORAGE_POSITION = keccak256("erc721"); + + /** + * @custom:storage-location erc8042:erc721 + */ + struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getStorage() internal pure returns (ERC721MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getERC721Storage() internal pure returns (ERC721Storage storage s) { + bytes32 position = ERC721_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the token collection name. + * @return The name of the token collection. + */ + function name() external view returns (string memory) { + return getStorage().name; + } + + /** + * @notice Returns the token collection symbol. + * @return The symbol of the token collection. + */ + function symbol() external view returns (string memory) { + return getStorage().symbol; + } + + /** + * @notice Provide the metadata URI for a given token ID. + * @param _tokenId tokenID of the NFT to query the metadata from + * @return the URI providing the detailed metadata of the specified tokenID + */ + function tokenURI(uint256 _tokenId) external view returns (string memory) { + ERC721MetadataStorage storage s = getStorage(); + ERC721Storage storage erc721Storage = getERC721Storage(); + address owner = erc721Storage.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (bytes(s.baseURI).length == 0) { + return ""; + } + if (_tokenId == 0) { + return string.concat(s.baseURI, "0"); + } + /** + * Convert _tokenId to string + */ + uint256 temp = _tokenId; + uint256 stringLength; + while (temp != 0) { + stringLength++; + temp /= 10; + } + bytes memory tokenIdString = new bytes(stringLength); + while (_tokenId != 0) { + stringLength--; + /** + * Convert each digit to its ASCII representation + * by adding 48 to get the ASCII code for the digit. + * Then store it in the byte array + */ + tokenIdString[stringLength] = bytes1(uint8(48 + (_tokenId % 10))); + _tokenId /= 10; + } + return string.concat(s.baseURI, string(tokenIdString)); + } + + /** + * @notice Exports the function selectors of the ERC721MetadataFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC721MetadataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.name.selector, this.symbol.selector, this.tokenURI.selector); + } +} diff --git a/cli/project-template/src/templates/token/ERC721/Metadata/ERC721MetadataMod.sol b/cli/project-template/src/templates/token/ERC721/Metadata/ERC721MetadataMod.sol new file mode 100644 index 00000000..c5be1f98 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Metadata/ERC721MetadataMod.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/** + * @notice Error indicating that the queried token does not exist. + */ +error ERC721NonexistentToken(uint256 _tokenId); + +/** + * @notice Error indicating the queried owner address is invalid (zero address). + */ +error ERC721InvalidOwner(address _owner); + +bytes32 constant STORAGE_POSITION = keccak256("erc721.metadata"); + +/** + * @custom:storage-location erc8042:erc721.metadata + */ +struct ERC721MetadataStorage { + string name; + string symbol; + string baseURI; +} + +/** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ +function getStorage() pure returns (ERC721MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +function setMetadata(string memory _name, string memory _symbol, string memory _baseURI) { + ERC721MetadataStorage storage s = getStorage(); + s.name = _name; + s.symbol = _symbol; + s.baseURI = _baseURI; +} diff --git a/cli/project-template/src/templates/token/ERC721/Mint/ERC721MintMod.sol b/cli/project-template/src/templates/token/ERC721/Mint/ERC721MintMod.sol new file mode 100644 index 00000000..17a9d158 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Mint/ERC721MintMod.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender The invalid sender address. + */ +error ERC721InvalidSender(address _sender); + +/** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver The invalid receiver address. + */ +error ERC721InvalidReceiver(address _receiver); + +/** + * @notice Emitted when ownership of a token changes, including minting and burning. + * @param _from The address transferring the token, or zero for minting. + * @param _to The address receiving the token, or zero for burning. + * @param _tokenId The ID of the token being transferred. + */ +event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + +/** + * @dev Storage position constant defined via keccak256 hash of diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc721"); + +/* + * @custom:storage-location erc8042:erc721 + * @notice Storage layout for ERC-721 token management. + * @dev Defines ownership, balances, approvals, and operator mappings per ERC-721 standard. + */ +struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +} + +/** + * @notice Returns the ERC-721 storage struct from its predefined slot. + * @dev Uses inline assembly to access diamond storage location. + * @return s The storage reference for ERC-721 state variables. + */ +function getStorage() pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Mints a new ERC-721 token to the specified address. + * @dev Reverts if the receiver address is zero or if the token already exists. + * @param _to The address that will own the newly minted token. + * @param _tokenId The ID of the token to mint. + */ +function mint(address _to, uint256 _tokenId) { + ERC721Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + if (s.ownerOf[_tokenId] != address(0)) { + revert ERC721InvalidSender(address(0)); + } + s.ownerOf[_tokenId] = _to; + unchecked { + s.balanceOf[_to]++; + } + emit Transfer(address(0), _to, _tokenId); +} + diff --git a/cli/project-template/src/templates/token/ERC721/Transfer/ERC721TransferFacet.sol b/cli/project-template/src/templates/token/ERC721/Transfer/ERC721TransferFacet.sol new file mode 100644 index 00000000..e6c040ef --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Transfer/ERC721TransferFacet.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/** + * @title ERC-721 Token Receiver Interface + * @notice Interface for contracts that want to handle safe transfers of ERC-721 tokens. + * @dev Contracts implementing this must return the selector to confirm token receipt. + */ +interface IERC721Receiver { + /** + * @notice Handles the receipt of an NFT. + * @param _operator The address which called `safeTransferFrom`. + * @param _from The previous owner of the token. + * @param _tokenId The NFT identifier being transferred. + * @param _data Additional data with no specified format. + * @return The selector to confirm the token transfer. + */ + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) + external + returns (bytes4); +} + +/** + * @title ERC-721 Token + * @notice A complete, dependency-free ERC-721 implementation using the diamond storage pattern. + * @dev This facet provides metadata, ownership, approvals, safe transfers, minting, burning, and helpers. + */ +contract ERC721TransferFacet { + /** + * @notice Error indicating that the queried token does not exist. + */ + error ERC721NonexistentToken(uint256 _tokenId); + + /** + * @notice Error indicating the sender does not match the token owner. + */ + error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + + /** + * @notice Error indicating the receiver address is invalid. + */ + error ERC721InvalidReceiver(address _receiver); + + /** + * @notice Error indicating the operator lacks approval to transfer the given token. + */ + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + /** + * @notice Emitted when ownership of an NFT changes by any mechanism. + */ + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + bytes32 constant STORAGE_POSITION = keccak256("erc721"); + + /** + * @custom:storage-location erc8042:erc721 + */ + struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + } + + /** + * @notice Returns a pointer to the ERC-721 storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The ERC721Storage struct in storage. + */ + function getStorage() internal pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @dev Internal function to transfer a token, checking for ownership and approval. + * @param _from The current owner of the token. + * @param _to The address to receive the token. + * @param _tokenId The token ID to transfer. + */ + function internalTransferFrom(address _from, address _to, uint256 _tokenId) internal { + ERC721Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (owner != _from) { + revert ERC721IncorrectOwner(_from, _tokenId, owner); + } + if (msg.sender != _from) { + if (!s.isApprovedForAll[_from][msg.sender] && msg.sender != s.approved[_tokenId]) { + revert ERC721InsufficientApproval(msg.sender, _tokenId); + } + } + delete s.approved[_tokenId]; + unchecked { + s.balanceOf[_from]--; + s.balanceOf[_to]++; + } + s.ownerOf[_tokenId] = _to; + emit Transfer(_from, _to, _tokenId); + } + + /** + * @notice Transfers a token from one address to another. + * @param _from The current owner of the token. + * @param _to The address to receive the token. + * @param _tokenId The token ID to transfer. + */ + function transferFrom(address _from, address _to, uint256 _tokenId) external { + internalTransferFrom(_from, _to, _tokenId); + } + + /** + * @notice Safely transfers a token, checking if the receiver can handle ERC-721 tokens. + * @param _from The current owner of the token. + * @param _to The address to receive the token. + * @param _tokenId The token ID to transfer. + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external { + internalTransferFrom(_from, _to, _tokenId); + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, "") returns (bytes4 returnValue) { + if (returnValue != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } + } + + /** + * @notice Safely transfers a token with additional data. + * @param _from The current owner of the token. + * @param _to The address to receive the token. + * @param _tokenId The token ID to transfer. + * @param _data Additional data with no specified format. + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external { + internalTransferFrom(_from, _to, _tokenId); + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4 returnValue) { + if (returnValue != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } + } + + /** + * @notice Exports the function selectors of the ERC721TransferFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC721TransferFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat( + this.transferFrom.selector, + bytes4(keccak256("safeTransferFrom(address,address,uint256)")), + bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)")) + ); + } +} diff --git a/cli/project-template/src/templates/token/ERC721/Transfer/ERC721TransferMod.sol b/cli/project-template/src/templates/token/ERC721/Transfer/ERC721TransferMod.sol new file mode 100644 index 00000000..bd964cd2 --- /dev/null +++ b/cli/project-template/src/templates/token/ERC721/Transfer/ERC721TransferMod.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title ERC-721 Transfer Module for Compose + */ + +/** + * @notice Thrown when attempting to interact with a non-existent token. + * @param _tokenId The ID of the token that does not exist. + */ +error ERC721NonexistentToken(uint256 _tokenId); + +/** + * @notice Thrown when the sender is not the owner of the token. + * @param _sender The address attempting the operation. + * @param _tokenId The ID of the token being transferred. + * @param _owner The actual owner of the token. + */ +error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + +/** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver The invalid receiver address. + */ +error ERC721InvalidReceiver(address _receiver); + +/** + * @notice Emitted when ownership of a token changes, including minting and burning. + * @param _from The address transferring the token, or zero for minting. + * @param _to The address receiving the token, or zero for burning. + * @param _tokenId The ID of the token being transferred. + */ +event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + +/** + * @dev Storage position constant defined via keccak256 hash of diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc721"); + +/** + * @notice Storage layout for ERC-721 token management. + * @dev Defines ownership, balances, approvals, and operator mappings per ERC-721 standard. + * @custom:storage-location erc8042:erc721 + */ +struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +} + +/** + * @notice Returns the ERC-721 storage struct from its predefined slot. + * @dev Uses inline assembly to access diamond storage location. + * @return s The storage reference for ERC-721 state variables. + */ +function getStorage() pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Transfers ownership of a token ID from one address to another. + * @dev Validates ownership, approval, and receiver address before updating state. + * @param _from The current owner of the token. + * @param _to The address that will receive the token. + * @param _tokenId The ID of the token being transferred. + */ +function transferFrom(address _from, address _to, uint256 _tokenId) { + ERC721Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (owner != _from) { + revert ERC721IncorrectOwner(_from, _tokenId, owner); + } + delete s.approved[_tokenId]; + unchecked { + s.balanceOf[_from]--; + s.balanceOf[_to]++; + } + s.ownerOf[_tokenId] = _to; + emit Transfer(_from, _to, _tokenId); +} + diff --git a/cli/project-template/src/templates/token/Royalty/RoyaltyFacet.sol b/cli/project-template/src/templates/token/Royalty/RoyaltyFacet.sol new file mode 100644 index 00000000..33cf00a5 --- /dev/null +++ b/cli/project-template/src/templates/token/Royalty/RoyaltyFacet.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title Royalty Facet - ERC-2981 NFT Royalty Standard Implementation + * @notice Implements royalty queries for NFT secondary sales per ERC-2981 standard. + * @dev Provides standardized royalty information to NFT marketplaces and platforms. + * Supports both default and per-token royalty configurations using diamond storage. + * This is an implementation of the ERC-2981 NFT Royalty Standard. + */ +contract RoyaltyFacet { + /** + * @notice Storage slot identifier for royalty storage. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc2981"); + + /** + * @dev The denominator with which to interpret royalty fees as a percentage of sale price. + * Expressed in basis points where 10000 = 100%. This value aligns with the ERC-2981 + * specification and marketplace expectations. + */ + uint96 constant FEE_DENOMINATOR = 10000; + + /** + * @notice Structure containing royalty information. + * @param receiver The address that will receive royalty payments. + * @param royaltyFraction The royalty fee expressed in basis points. + */ + struct RoyaltyInfo { + address receiver; + uint96 royaltyFraction; + } + + /** + * @custom:storage-location erc8042:erc2981 + */ + struct RoyaltyStorage { + RoyaltyInfo defaultRoyaltyInfo; + mapping(uint256 tokenId => RoyaltyInfo) tokenRoyaltyInfo; + } + + /** + * @notice Returns a pointer to the royalty storage struct. + * @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + * @return s The RoyaltyStorage struct in storage. + */ + function getStorage() internal pure returns (RoyaltyStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns royalty information for a given token and sale price. + * @dev Returns token-specific royalty if set, otherwise falls back to default royalty. + * Royalty amount is calculated as a percentage of the sale price using basis points. + * Implements the ERC-2981 royaltyInfo function. + * @param _tokenId The NFT asset queried for royalty information. + * @param _salePrice The sale price of the NFT asset specified by _tokenId. + * @return receiver The address designated to receive the royalty payment. + * @return royaltyAmount The royalty payment amount for _salePrice. + */ + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount) + { + RoyaltyStorage storage s = getStorage(); + RoyaltyInfo memory royalty = s.tokenRoyaltyInfo[_tokenId]; + + if (royalty.receiver == address(0)) { + royalty = s.defaultRoyaltyInfo; + } + + receiver = royalty.receiver; + royaltyAmount = (_salePrice * royalty.royaltyFraction) / FEE_DENOMINATOR; + } +} diff --git a/cli/project-template/src/templates/token/Royalty/RoyaltyMod.sol b/cli/project-template/src/templates/token/Royalty/RoyaltyMod.sol new file mode 100644 index 00000000..ecb64dcd --- /dev/null +++ b/cli/project-template/src/templates/token/Royalty/RoyaltyMod.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/* + * @title LibRoyalty - ERC-2981 Royalty Standard Library + * @notice Provides internal functions and storage layout for ERC-2981 royalty logic. + * @dev Uses ERC-8042 for storage location standardization. Compatible with OpenZeppelin's ERC2981 behavior. + * This is an implementation of the ERC-2981 NFT Royalty Standard. + */ + +/** + * @notice Thrown when default royalty fee exceeds 100% (10000 basis points). + * @param _numerator The fee numerator that exceeds the denominator. + * @param _denominator The fee denominator (10000 basis points). + */ +error ERC2981InvalidDefaultRoyalty(uint256 _numerator, uint256 _denominator); + +/** + * @notice Thrown when default royalty receiver is the zero address. + * @param _receiver The invalid receiver address. + */ +error ERC2981InvalidDefaultRoyaltyReceiver(address _receiver); + +/** + * @notice Thrown when token-specific royalty fee exceeds 100% (10000 basis points). + * @param _tokenId The token ID with invalid royalty configuration. + * @param _numerator The fee numerator that exceeds the denominator. + * @param _denominator The fee denominator (10000 basis points). + */ +error ERC2981InvalidTokenRoyalty(uint256 _tokenId, uint256 _numerator, uint256 _denominator); + +/** + * @notice Thrown when token-specific royalty receiver is the zero address. + * @param _tokenId The token ID with invalid royalty configuration. + * @param _receiver The invalid receiver address. + */ +error ERC2981InvalidTokenRoyaltyReceiver(uint256 _tokenId, address _receiver); + +bytes32 constant STORAGE_POSITION = keccak256("erc2981"); + +/** + * @dev The denominator with which to interpret royalty fees as a percentage of sale price. + * Expressed in basis points where 10000 = 100%. This value aligns with the ERC-2981 + * specification and marketplace expectations. Implemented as a constant for gas efficiency + * rather than the virtual function pattern, as Compose does not support inheritance-based + * customization. To modify this value, deploy a custom facet implementation. + */ +uint96 constant FEE_DENOMINATOR = 10000; + +/** + * @notice Structure containing royalty information. + * @param receiver The address that will receive royalty payments. + * @param royaltyFraction The royalty fee expressed in basis points. + */ +struct RoyaltyInfo { + address receiver; + uint96 royaltyFraction; +} + +/** + * @custom:storage-location erc8042:erc2981 + */ +struct RoyaltyStorage { + RoyaltyInfo defaultRoyaltyInfo; + mapping(uint256 tokenId => RoyaltyInfo) tokenRoyaltyInfo; +} + +/** + * @notice Returns the royalty storage struct from its predefined slot. + * @dev Uses inline assembly to access diamond storage location. + * @return s The storage reference for royalty state variables. + */ +function getStorage() pure returns (RoyaltyStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Queries royalty information for a given token and sale price. + * @dev Returns token-specific royalty or falls back to default royalty. + * Royalty amount is calculated as a percentage of the sale price using basis points. + * Implements the ERC-2981 royaltyInfo function logic. + * @param _tokenId The NFT asset queried for royalty information. + * @param _salePrice The sale price of the NFT asset. + * @return receiver The address designated to receive the royalty payment. + * @return royaltyAmount The royalty payment amount for _salePrice. + */ +function royaltyInfo(uint256 _tokenId, uint256 _salePrice) view returns (address receiver, uint256 royaltyAmount) { + RoyaltyStorage storage s = getStorage(); + RoyaltyInfo memory royalty = s.tokenRoyaltyInfo[_tokenId]; + + if (royalty.receiver == address(0)) { + royalty = s.defaultRoyaltyInfo; + } + + receiver = royalty.receiver; + royaltyAmount = (_salePrice * royalty.royaltyFraction) / FEE_DENOMINATOR; +} + +/** + * @notice Sets the default royalty information that applies to all tokens. + * @dev Validates receiver and fee, then updates default royalty storage. + * @param _receiver The royalty recipient address. + * @param _feeNumerator The royalty fee in basis points. + */ +function setDefaultRoyalty(address _receiver, uint96 _feeNumerator) { + if (_feeNumerator > FEE_DENOMINATOR) { + revert ERC2981InvalidDefaultRoyalty(_feeNumerator, FEE_DENOMINATOR); + } + if (_receiver == address(0)) { + revert ERC2981InvalidDefaultRoyaltyReceiver(address(0)); + } + + RoyaltyStorage storage s = getStorage(); + s.defaultRoyaltyInfo = RoyaltyInfo(_receiver, _feeNumerator); +} + +/** + * @notice Removes default royalty information. + * @dev After calling this function, royaltyInfo will return (address(0), 0) for tokens without specific royalty. + */ +function deleteDefaultRoyalty() { + RoyaltyStorage storage s = getStorage(); + delete s.defaultRoyaltyInfo; +} + +/** + * @notice Sets royalty information for a specific token, overriding the default. + * @dev Validates receiver and fee, then updates token-specific royalty storage. + * @param _tokenId The token ID to configure royalty for. + * @param _receiver The royalty recipient address. + * @param _feeNumerator The royalty fee in basis points. + */ +function setTokenRoyalty(uint256 _tokenId, address _receiver, uint96 _feeNumerator) { + if (_feeNumerator > FEE_DENOMINATOR) { + revert ERC2981InvalidTokenRoyalty(_tokenId, _feeNumerator, FEE_DENOMINATOR); + } + if (_receiver == address(0)) { + revert ERC2981InvalidTokenRoyaltyReceiver(_tokenId, address(0)); + } + + RoyaltyStorage storage s = getStorage(); + s.tokenRoyaltyInfo[_tokenId] = RoyaltyInfo(_receiver, _feeNumerator); +} + +/** + * @notice Resets royalty information for a specific token to use the default setting. + * @dev Clears token-specific royalty storage, causing fallback to default royalty. + * @param _tokenId The token ID to reset royalty configuration for. + */ +function resetTokenRoyalty(uint256 _tokenId) { + RoyaltyStorage storage s = getStorage(); + delete s.tokenRoyaltyInfo[_tokenId]; +} diff --git a/cli/project-template/src/utils/files.ts b/cli/project-template/src/utils/files.ts new file mode 100644 index 00000000..7d8603bd --- /dev/null +++ b/cli/project-template/src/utils/files.ts @@ -0,0 +1,62 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { parseSolidityImports } from "./solidityText"; + +// Check whether a filesystem path exists without throwing to callers. +export async function pathExists(target: string): Promise { + try { + await fs.access(target); + return true; + } catch { + return false; + } +} + +// Copy a file only when the destination does not already exist. +export async function copyFileIfMissing(from: string, to: string): Promise { + try { + await fs.access(to); + return; + } catch { + // destination does not exist, continue + } + await fs.mkdir(path.dirname(to), { recursive: true }); + await fs.copyFile(from, to); +} + +// Write text content only when the target file does not already exist. +export async function writeFileIfMissing(to: string, content: string): Promise { + try { + await fs.access(to); + return; + } catch { + // destination does not exist, continue + } + await fs.mkdir(path.dirname(to), { recursive: true }); + await fs.writeFile(to, content, "utf8"); +} + +// Resolve local Solidity imports reachable from a set of seed source files. +export async function resolveLocalSolidityImportClosure(seedSources: string[]): Promise> { + const visited = new Set(); + const stack = [...seedSources]; + + while (stack.length > 0) { + const file = path.resolve(stack.pop() as string); + if (visited.has(file)) continue; + visited.add(file); + + const code = await fs.readFile(file, "utf8"); + for (const specifier of parseSolidityImports(code)) { + if (!specifier.startsWith(".")) { + continue; + } + const dep = path.resolve(path.dirname(file), specifier); + if (!visited.has(dep)) { + stack.push(dep); + } + } + } + + return visited; +} diff --git a/cli/project-template/src/utils/regex.ts b/cli/project-template/src/utils/regex.ts new file mode 100644 index 00000000..66ba7fb6 --- /dev/null +++ b/cli/project-template/src/utils/regex.ts @@ -0,0 +1,4 @@ +// Escape user-provided text so it can be safely embedded in a RegExp. +export function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/cli/project-template/src/utils/solidityText.ts b/cli/project-template/src/utils/solidityText.ts new file mode 100644 index 00000000..15a55100 --- /dev/null +++ b/cli/project-template/src/utils/solidityText.ts @@ -0,0 +1,88 @@ +export type SolidityFunctionInfo = { + name: string; + signature: string; + visibility: "external" | "public"; +}; + +// Parse Solidity import specifiers from source text. +export function parseSolidityImports(soliditySource: string): string[] { + const imports: string[] = []; + const re = /^\s*import\s+(?:[^'"]+from\s+)?["']([^"']+)["'];/gm; + let m: RegExpExecArray | null; + while ((m = re.exec(soliditySource)) !== null) { + imports.push(m[1]); + } + return imports; +} + +// Remove block and line comments from Solidity source text. +export function removeSolidityComments(source: string): string { + return source + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/\/\/.*$/gm, ""); +} + +// Normalize one Solidity parameter type by removing location and parameter name. +export function normalizeSolidityParameterType(parameter: string): string { + const withoutName = parameter + .trim() + .replace(/\s+/g, " ") + .replace(/\b(calldata|memory|storage|indexed)\b/g, "") + .trim(); + + const parts = withoutName.split(" ").filter(Boolean); + if (parts.length <= 1) { + return withoutName; + } + + return parts.slice(0, -1).join(" "); +} + +// Parse public and external Solidity functions with normalized signatures. +export function parseSolidityFunctions(source: string): SolidityFunctionInfo[] { + const cleanSource = removeSolidityComments(source); + const functions: SolidityFunctionInfo[] = []; + const functionPattern = /\bfunction\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*([^;{]*)/g; + let match: RegExpExecArray | null; + + while ((match = functionPattern.exec(cleanSource)) !== null) { + const [, name, rawParameters, tail] = match; + const visibilityMatch = tail.match(/\b(external|public)\b/); + if (!visibilityMatch || name === "exportSelectors") { + continue; + } + + const parameterTypes = rawParameters + .split(",") + .map((parameter) => parameter.trim()) + .filter(Boolean) + .map(normalizeSolidityParameterType); + + functions.push({ + name, + signature: `${name}(${parameterTypes.join(",")})`, + visibility: visibilityMatch[1] as "external" | "public", + }); + } + + return functions; +} + +// Parse function names exported through `this..selector` entries. +export function parseExportedSelectorNames(source: string): string[] { + const cleanSource = removeSolidityComments(source); + const exportMatch = cleanSource.match(/\bfunction\s+exportSelectors\s*\([^)]*\)[^{;]*\{([\s\S]*?)\n\s*\}/); + if (!exportMatch) { + return []; + } + + const exported = new Set(); + const selectorPattern = /\bthis\.([A-Za-z_][A-Za-z0-9_]*)\.selector\b/g; + let match: RegExpExecArray | null; + + while ((match = selectorPattern.exec(exportMatch[1])) !== null) { + exported.add(match[1]); + } + + return [...exported]; +} diff --git a/cli/project-template/src/utils/terminal.ts b/cli/project-template/src/utils/terminal.ts new file mode 100644 index 00000000..f0c26ca7 --- /dev/null +++ b/cli/project-template/src/utils/terminal.ts @@ -0,0 +1,14 @@ +// Wrap terminal text in ANSI cyan color codes. +export function cyan(text: string): string { + return `\u001b[36m${text}\u001b[39m`; +} + +// Wrap terminal text in ANSI red color codes. +export function red(text: string): string { + return `\u001b[31m${text}\u001b[39m`; +} + +// Wrap terminal text in ANSI yellow color codes. +export function yellow(text: string): string { + return `\u001b[33m${text}\u001b[39m`; +} diff --git a/cli/project-template/tsconfig.json b/cli/project-template/tsconfig.json new file mode 100644 index 00000000..59088f9e --- /dev/null +++ b/cli/project-template/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +}