├── .gas-snapshot ├── .github └── workflows │ ├── benchmark.yml │ ├── formatter.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── assets ├── core-extension-flow-example.png └── readme-hero-image.png ├── coverage.md ├── design-document.md ├── foundry.toml ├── package.json ├── remappings.txt ├── script ├── benchmark-ext │ └── erc721 │ │ ├── BenchmarkERC721.s.sol │ │ ├── BenchmarkERC721Base.sol │ │ ├── BenchmarkERC721Manifold.sol │ │ └── BenchmarkERC721ThirdwebLegacy.sol └── superchain │ ├── DeployTWCloneFactory.s.sol │ ├── IL2ToL2CrossDomainMessenger.sol │ ├── README.md │ ├── SuperChainBridge.sol │ ├── SuperChainERC20Setup.s.sol │ └── SuperChainInterop.s.sol ├── src ├── Core.sol ├── Module.sol ├── Role.sol ├── TWCloneFactory.sol ├── callback │ ├── AfterWithdrawCallback.sol │ ├── BeforeApproveCallbackERC20.sol │ ├── BeforeApproveCallbackERC721.sol │ ├── BeforeApproveForAllCallback.sol │ ├── BeforeBatchMintCallbackERC1155.sol │ ├── BeforeBatchTransferCallbackERC1155.sol │ ├── BeforeBurnCallbackERC1155.sol │ ├── BeforeBurnCallbackERC20.sol │ ├── BeforeBurnCallbackERC721.sol │ ├── BeforeDistributeCallback.sol │ ├── BeforeMintCallbackERC1155.sol │ ├── BeforeMintCallbackERC20.sol │ ├── BeforeMintCallbackERC721.sol │ ├── BeforeMintWithSignatureCallbackERC1155.sol │ ├── BeforeMintWithSignatureCallbackERC20.sol │ ├── BeforeMintWithSignatureCallbackERC721.sol │ ├── BeforeTransferCallbackERC1155.sol │ ├── BeforeTransferCallbackERC20.sol │ ├── BeforeTransferCallbackERC721.sol │ ├── OnTokenURICallback.sol │ ├── UpdateMetadataCallbackERC1155.sol │ ├── UpdateMetadataCallbackERC721.sol │ └── UpdateTokenIdERC1155.sol ├── core │ ├── MintFeeManagerCore.sol │ ├── SplitFeesCore.sol │ ├── SplitWallet.sol │ └── token │ │ ├── ERC1155Base.sol │ │ ├── ERC1155Core.sol │ │ ├── ERC1155CoreInitializable.sol │ │ ├── ERC20Base.sol │ │ ├── ERC20Core.sol │ │ ├── ERC20CoreInitializable.sol │ │ ├── ERC721Base.sol │ │ ├── ERC721Core.sol │ │ └── ERC721CoreInitializable.sol ├── interface │ ├── ICore.sol │ ├── IERC165.sol │ ├── IERC20.sol │ ├── IERC20Metadata.sol │ ├── IERC7572.sol │ ├── IInstallationCallback.sol │ ├── IMintFeeManager.sol │ ├── IModule.sol │ ├── IModuleConfig.sol │ └── ISplitWallet.sol ├── libraries │ ├── Cast.sol │ ├── ShortString.sol │ └── Split.sol └── module │ ├── MintFeeManagerModule.sol │ ├── SplitFeesModule.sol │ └── token │ ├── crosschain │ ├── CrossChain.sol │ └── SuperChainInterop.sol │ ├── metadata │ ├── BatchMetadataERC1155.sol │ ├── BatchMetadataERC721.sol │ ├── OpenEditionMetadataERC1155.sol │ └── OpenEditionMetadataERC721.sol │ ├── minting │ ├── ClaimableERC1155.sol │ ├── ClaimableERC20.sol │ ├── ClaimableERC721.sol │ ├── MintableERC1155.sol │ ├── MintableERC20.sol │ └── MintableERC721.sol │ ├── royalty │ ├── RoyaltyERC1155.sol │ └── RoyaltyERC721.sol │ ├── tokenId │ └── SequentialTokenIdERC1155.sol │ └── transferable │ ├── CreatorTokenERC20.sol │ ├── TransferableERC1155.sol │ ├── TransferableERC20.sol │ └── TransferableERC721.sol └── test ├── Interface.t.sol ├── ModularCore.t.sol ├── TWCloneFactory.t.sol ├── benchmark └── CoreBenchmark.t.sol ├── core ├── ERC1155Core.t.sol ├── ERC20Core.t.sol └── ERC721Core.t.sol ├── mock └── MockMintFeeManager.sol ├── module ├── MintFeeManager.t.sol ├── SplitFeeModule.t.sol ├── SplitWallet.t.sol ├── crosschain │ └── SuperChainInterop.t.sol ├── metadata │ ├── BatchMetadataERC1155.t.sol │ ├── BatchMetadataERC721.t.sol │ ├── OpenEditionMetadataERC1155.t.sol │ └── OpenEditionMetadataERC721.t.sol ├── minting │ ├── ClaimableERC1155.t.sol │ ├── ClaimableERC20.t.sol │ ├── ClaimableERC721.t.sol │ ├── MintableERC1155.t.sol │ ├── MintableERC20.t.sol │ └── MintableERC721.t.sol ├── royalty │ ├── RoyaltyERC1155.t.sol │ ├── RoyaltyERC721.t.sol │ └── RoyaltyUtils.sol ├── tokenId │ └── SequentialTokenIdERC1155.t.sol └── transferable │ ├── CreatorTokenERC20.t.sol │ ├── CreatorTokenUtils.sol │ ├── TransferableERC1155.t.sol │ ├── TransferableERC20.t.sol │ └── TransferableERC721.t.sol └── utils ├── ExtensionProxyFactory.sol └── TestPlus.sol /.gas-snapshot: -------------------------------------------------------------------------------- 1 | CoreBenchmark:test_core_callCallbackFunction_required() (gas: 67409) 2 | CoreBenchmark:test_core_callFunction_callback_callbackFunctionRequired() (gas: 65131) 3 | CoreBenchmark:test_core_callFunction_notPermissionedDelegate() (gas: 39115) 4 | CoreBenchmark:test_core_callFunction_notPermissionedExternal() (gas: 39093) 5 | CoreBenchmark:test_deployCore() (gas: 2139892) 6 | CoreBenchmark:test_deployModule() (gas: 350173) 7 | CoreBenchmark:test_installModule() (gas: 352153) 8 | CoreBenchmark:test_module_callFunction_notPermissionedDelegate() (gas: 29381) 9 | CoreBenchmark:test_module_callFunction_notPermissionedExternal() (gas: 29379) 10 | CoreBenchmark:test_uninstallModule() (gas: 91332) -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: benchmark 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # cancel previous runs if new commits are pushed to the branch 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | env: 17 | FOUNDRY_PROFILE: ci 18 | 19 | jobs: 20 | check: 21 | strategy: 22 | fail-fast: true 23 | 24 | name: Foundry 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | submodules: recursive 30 | 31 | - name: Install Foundry 32 | uses: foundry-rs/foundry-toolchain@v1 33 | with: 34 | version: nightly 35 | 36 | - name: Run Forge build 37 | run: | 38 | forge --version 39 | forge build --sizes 40 | id: build 41 | 42 | - name: Create Forge Gas Report 43 | run: | 44 | forge snapshot --diff --isolate --mp 'test/benchmark/*' 45 | id: benchmark 46 | -------------------------------------------------------------------------------- /.github/workflows/formatter.yml: -------------------------------------------------------------------------------- 1 | name: formatter 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # cancel previous runs if new commits are pushed to the branch 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | env: 17 | FOUNDRY_PROFILE: ci 18 | 19 | jobs: 20 | check: 21 | strategy: 22 | fail-fast: true 23 | 24 | name: Foundry 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | submodules: recursive 30 | 31 | - name: Install Foundry 32 | uses: foundry-rs/foundry-toolchain@v1 33 | with: 34 | version: nightly 35 | 36 | - name: Formatter 37 | run: | 38 | forge fmt --check test src script 39 | id: formatter 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # cancel previous runs if new commits are pushed to the branch 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | env: 17 | FOUNDRY_PROFILE: ci 18 | 19 | jobs: 20 | check: 21 | strategy: 22 | fail-fast: true 23 | 24 | name: Foundry 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | submodules: recursive 30 | 31 | - name: Install Foundry 32 | uses: foundry-rs/foundry-toolchain@v1 33 | with: 34 | version: nightly 35 | 36 | - name: Run Forge build 37 | run: | 38 | forge --version 39 | forge build --sizes 40 | id: build 41 | 42 | - name: Run Forge tests 43 | run: | 44 | forge test -vvv 45 | id: test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solady"] 5 | path = lib/solady 6 | url = https://github.com/vectorized/solady 7 | [submodule "lib/erc721a"] 8 | path = lib/erc721a 9 | url = https://github.com/chiru-labs/erc721a 10 | [submodule "lib/ERC721A-Upgradeable"] 11 | path = lib/ERC721A-Upgradeable 12 | url = https://github.com/chiru-labs/ERC721A-Upgradeable 13 | [submodule "lib/creator-token-contracts"] 14 | path = lib/creator-token-contracts 15 | url = https://github.com/limitbreakinc/creator-token-contracts 16 | [submodule "lib/creator-token-standards"] 17 | path = lib/creator-token-standards 18 | url = https://github.com/limitbreakinc/creator-token-standards 19 | [submodule "lib/PermitC"] 20 | path = lib/PermitC 21 | url = https://github.com/limitbreakinc/PermitC 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 |
5 |

6 |

Modular Contracts

7 |

Write smart contracts for which you can add, remove, upgrade or switch out the exact parts you want.

8 |
9 | 10 | A Modular Contract is built of two kinds of parts: a _Modular Core_ and its _Modular Modules_. 11 | 12 | ![modular-contracts-analogy](./assets/readme-hero-image.png) 13 | 14 | A developer writes a **_Core_** smart contract as the foundation that can be customized by adding new parts and updating or removing these parts over time. These ‘parts’ are **_Module_** smart contracts which any third-party developer can independently develop with reference to the **_Core_** smart contract as the known foundation to build around. 15 | 16 | # Install and Use 17 | 18 | This project can currently be installed as a dependency in [foundry](https://book.getfoundry.sh/) projects. To install, run: 19 | 20 | ```bash 21 | forge install https://github.com/thirdweb-dev/modular-contracts 22 | ``` 23 | 24 | Add the following in a `remappings.txt` file: 25 | 26 | ``` 27 | @modular-contracts/=lib/modular-contracts/src/ 28 | ``` 29 | 30 | Import `Core` inherit to build a Modular Core contract (e.g. ERC-721 Core): 31 | 32 | ```solidity 33 | import {Core} from "@modular-contracts/Core.sol"; 34 | import {ERC721A} from "@erc721a/extensions/ERC721AQueryable.sol"; 35 | 36 | contract ModularNFTCollection is ERC721A, Core {} 37 | ``` 38 | 39 | Import `Module` to create an Module for your Core contract (e.g. `Soulbound`): 40 | 41 | ```solidity 42 | import {Module} from "@modular-contracts/Module.sol"; 43 | 44 | contract SoulboundERC721 is Module {} 45 | ``` 46 | 47 | # Run this repo 48 | 49 | Clone the repo: 50 | 51 | ```bash 52 | git clone https://github.com/thirdweb-dev/modular-contracts.git 53 | ``` 54 | 55 | Install dependencies: 56 | 57 | ```bash 58 | # Install dependecies 59 | forge install 60 | ``` 61 | 62 | 77 | 78 | # Feedback 79 | 80 | If you have any feedback, please create an issue or reach out to us at support@thirdweb.com. 81 | 82 | # Authors 83 | 84 | - [thirdweb](https://thirdweb.com) 85 | 86 | # License 87 | 88 | [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) 89 | -------------------------------------------------------------------------------- /assets/core-extension-flow-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/modular-contracts/201fef1107bee8b89beb78b973103601e072d476/assets/core-extension-flow-example.png -------------------------------------------------------------------------------- /assets/readme-hero-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/modular-contracts/201fef1107bee8b89beb78b973103601e072d476/assets/readme-hero-image.png -------------------------------------------------------------------------------- /coverage.md: -------------------------------------------------------------------------------- 1 | Uncovered for src/module/token/royalty/RoyaltyERC1155.sol: 2 | - Function "encodeBytesOnUninstall" (location: source ID 105, line 166, chars 6329-6426, hits: 0) 3 | - Line (location: source ID 105, line 167, chars 6410-6419, hits: 0) 4 | - Statement (location: source ID 105, line 167, chars 6410-6419, hits: 0) 5 | - Branch (branch: 0, path: 1) (location: source ID 105, line 182, chars 6937-7182, hits: 0) 6 | - Branch (branch: 1, path: 1) (location: source ID 105, line 200, chars 7464-7917, hits: 0) 7 | - Line (location: source ID 105, line 211, chars 7871-7874, hits: 0) 8 | - Statement (location: source ID 105, line 211, chars 7871-7874, hits: 0) 9 | - Function "onUninstall" (location: source ID 105, line 229, chars 8450-8503, hits: 0) 10 | - Branch (branch: 2, path: 0) (location: source ID 105, line 286, chars 10597-10670, hits: 0) 11 | - Line (location: source ID 105, line 287, chars 10630-10659, hits: 0) 12 | - Statement (location: source ID 105, line 287, chars 10630-10659, hits: 0) 13 | - Function "getTransferValidationFunction" (location: source ID 105, line 307, chars 11282-11619, hits: 0) 14 | - Line (location: source ID 105, line 312, chars 11431-11581, hits: 0) 15 | - Statement (location: source ID 105, line 312, chars 11431-11581, hits: 0) 16 | - Line (location: source ID 105, line 317, chars 11591-11612, hits: 0) 17 | - Statement (location: source ID 105, line 317, chars 11591-11612, hits: 0) 18 | - Branch (branch: 3, path: 0) (location: source ID 105, line 332, chars 12045-12125, hits: 0) 19 | - Line (location: source ID 105, line 333, chars 12085-12114, hits: 0) 20 | - Statement (location: source ID 105, line 333, chars 12085-12114, hits: 0) 21 | - Statement (location: source ID 105, line 376, chars 13428-13454, hits: 0) 22 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc-version = "0.8.26" 3 | evm_version = "paris" 4 | optimizer = true 5 | optimizer_runs = 10000 6 | cache = true 7 | src = "src" 8 | out = "out" 9 | libs = ["lib"] 10 | ffi = true 11 | gas_reports = [ 12 | "ERC20Core", 13 | "ERC721Core", 14 | "ERC1155Core" 15 | ] 16 | ignored_warnings_from = ["node_modules", "lib", "test"] 17 | fs_permissions = [{ access = "read", path = "./"}] 18 | 19 | [fmt] 20 | line_length = 120 21 | tab_width = 4 22 | sort_imports = true 23 | override_spacing = false 24 | bracket_spacing = false 25 | contract_new_lines = true 26 | single_line_statement_blocks = "multi" 27 | int_types = "long" 28 | quote_style = "double" 29 | number_underscore = "thousands" 30 | multiline_func_header = "attributes_first" 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contracts-next", 3 | "version": "1.0.0", 4 | "repository": "https://github.com/thirdweb-dev/contracts-next.git", 5 | "author": "Krishang ", 6 | "license": "Apache-2.0", 7 | "devDependencies": {}, 8 | "dependencies": {}, 9 | "scripts": {} 10 | } 11 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | @solady/=lib/solady/src/ 4 | @erc721a/=lib/erc721a/contracts/ 5 | @erc721a-upgradeable/=lib/ERC721A-Upgradeable/contracts/ 6 | @limitbreak/creator-token-standards/=lib/creator-token-standards/src/ 7 | @limitbreak/permit-c/=lib/PermitC/src/ 8 | -------------------------------------------------------------------------------- /script/benchmark-ext/erc721/BenchmarkERC721.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.13; 3 | 4 | import {IERC721} from "./BenchmarkERC721Base.sol"; 5 | 6 | import {BenchmarkERC721Manifold} from "./BenchmarkERC721Manifold.sol"; 7 | import {BenchmarkERC721ThirdwebLegacy} from "./BenchmarkERC721ThirdwebLegacy.sol"; 8 | import "forge-std/Script.sol"; 9 | 10 | contract BenchmarkERC721 is Script { 11 | 12 | function benchmarkThirdwebLegacy() public { 13 | BenchmarkERC721ThirdwebLegacy benchmark = new BenchmarkERC721ThirdwebLegacy(); 14 | 15 | address contractAddress = benchmark.deployContract( 16 | address(benchmark), "BenchmarkERC721", "B721", "ipfs://QmSkieqXz9voc614Q5LAhQY13LjX6bUzqp2dpDdJhARL7X/0" 17 | ); 18 | benchmark.mintToken(contractAddress); 19 | IERC721(contractAddress).setApprovalForAll(address(benchmark), true); 20 | benchmark.transferTokenFrom(contractAddress); 21 | } 22 | 23 | function benchmarkManifold() public { 24 | BenchmarkERC721Manifold benchmark = new BenchmarkERC721Manifold(); 25 | address contractAddress = benchmark.deployContract(address(0), "BenchmarkERC721", "B721", ""); 26 | benchmark.mintToken(contractAddress); 27 | IERC721(contractAddress).setApprovalForAll(address(benchmark), true); 28 | benchmark.transferTokenFrom(contractAddress); 29 | } 30 | 31 | function benchmarkThirdwebNext() public {} 32 | 33 | function benchmarkZora() public { 34 | // premint 35 | // https://sepolia.explorer.zora.energy/tx/0x1daffd513abf5a8ab1b73bbc9c9dbb1fb576c64069a9a5b6afd2f34ccda5caf6 36 | } 37 | 38 | function run() external { 39 | vm.startBroadcast(); 40 | benchmarkThirdwebLegacy(); 41 | benchmarkManifold(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /script/benchmark-ext/erc721/BenchmarkERC721Base.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.24; 3 | 4 | interface IERC721 { 5 | 6 | function transferFrom(address from, address to, uint256 tokenId) external; 7 | 8 | function setApprovalForAll(address operator, bool _approved) external; 9 | 10 | } 11 | 12 | abstract contract BenchmarkERC721Base { 13 | 14 | // Step 1: Deploy a contract through factory or create 15 | // Step 2: Setup the collection with token URI 16 | // Step 3: Mint an NFT 17 | // Step 4: Mint 100 NFTs 18 | // Step 5: Transfer 1 Token 19 | function deployContract( 20 | address deployerAddress, 21 | string memory name, 22 | string memory symbol, 23 | string memory contractURI 24 | ) external virtual returns (address contractAddress); 25 | 26 | function setupToken(address contractAddress) external virtual; 27 | 28 | function mintToken(address contractAddress) external virtual; 29 | 30 | function mintBatchTokens(address contractAddress) external virtual; 31 | 32 | function transferTokenFrom(address contractAddress) external virtual; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /script/benchmark-ext/erc721/BenchmarkERC721Manifold.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.24; 3 | 4 | import "./BenchmarkERC721Base.sol"; 5 | 6 | library StorageSlot { 7 | 8 | struct AddressSlot { 9 | address value; 10 | } 11 | 12 | function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { 13 | /// @solidity memory-safe-assembly 14 | assembly { 15 | r.slot := slot 16 | } 17 | } 18 | 19 | } 20 | 21 | abstract contract Proxy { 22 | 23 | function _delegate(address implementation) internal virtual { 24 | assembly { 25 | calldatacopy(0, 0, calldatasize()) 26 | 27 | let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) 28 | 29 | returndatacopy(0, 0, returndatasize()) 30 | 31 | switch result 32 | case 0 { revert(0, returndatasize()) } 33 | default { return(0, returndatasize()) } 34 | } 35 | } 36 | 37 | function _implementation() internal view virtual returns (address); 38 | 39 | function _fallback() internal virtual { 40 | _delegate(_implementation()); 41 | } 42 | 43 | fallback() external payable virtual { 44 | _fallback(); 45 | } 46 | 47 | } 48 | 49 | contract ERC721Creator is Proxy { 50 | 51 | constructor(string memory name, string memory symbol) { 52 | assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); 53 | StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = 0x07aee92b7C5977F5EC15d20BaC713A21f72F287B; 54 | (bool success,) = 0x07aee92b7C5977F5EC15d20BaC713A21f72F287B.delegatecall( 55 | abi.encodeWithSignature("initialize(string,string)", name, symbol) 56 | ); 57 | require(success, "Initialization failed"); 58 | } 59 | 60 | bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 61 | 62 | function implementation() public view returns (address) { 63 | return _implementation(); 64 | } 65 | 66 | function _implementation() internal view override returns (address) { 67 | return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; 68 | } 69 | 70 | } 71 | 72 | interface IManifoldNFT { 73 | 74 | function transferFrom(address from, address to, uint256 tokenId) external; 75 | 76 | function mintBase(address to, string calldata uri) external returns (uint256); 77 | 78 | function mintBaseBatch(address to, uint16 count) external returns (uint256[] memory); 79 | 80 | function mintBaseBatch(address to, string[] calldata uris) external returns (uint256[] memory); 81 | 82 | } 83 | 84 | contract BenchmarkERC721Manifold is BenchmarkERC721Base { 85 | 86 | function deployContract( 87 | address deployerAddress, 88 | string memory name, 89 | string memory symbol, 90 | string memory contractURI 91 | ) external override returns (address contractAddress) { 92 | //Reference: 93 | // https://sepolia.etherscan.io/address/0xfc958641e52563f071534495886a8ac590dcbfa2#code 94 | // https://sepolia.etherscan.io/tx/0xbc029723fbfd3cfef3c47c523bc9a4e7f073d6f0e89de3f5761701adbd4118a8 95 | contractAddress = address(new ERC721Creator(name, symbol)); 96 | } 97 | 98 | function setupToken(address contractAddress) external override {} 99 | 100 | function mintToken(address contractAddress) external override { 101 | // ERC721 Mint token 102 | // https://sepolia.etherscan.io/tx/0x05e54d052b160cf46e8133c2cb4fb5ecfec754e791ae8dd7197a02d332531899 103 | // https://sepolia.etherscan.io/tx/0x6815e65fc7376ba80daa8cbe0bd1bd63476bf78639a554702f981b5545ba0732 104 | IManifoldNFT(contractAddress).mintBase( 105 | tx.origin, "https://studio.api.manifoldxyz.dev/asset_uploader/1/asset/3356006855/metadata/full" 106 | ); 107 | } 108 | 109 | function mintBatchTokens(address contractAddress) external override {} 110 | 111 | function transferTokenFrom(address contractAddress) external override { 112 | IERC721(contractAddress).transferFrom(tx.origin, address(0xdead), 1); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /script/benchmark-ext/erc721/BenchmarkERC721ThirdwebLegacy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.24; 3 | 4 | import "./BenchmarkERC721Base.sol"; 5 | 6 | interface IBenchmarkERC721 { 7 | 8 | function deployProxyByImplementation(address _implementation, bytes calldata _data, bytes32 _salt) 9 | external 10 | returns (address deployedProxy); 11 | 12 | function initialize( 13 | address _defaultAdmin, 14 | string memory _name, 15 | string memory _symbol, 16 | string memory _contractURI, 17 | address[] memory _trustedForwarders, 18 | address _saleRecipient, 19 | address _royaltyRecipient, 20 | uint128 _royaltyBps, 21 | uint128 _platformFeeBps, 22 | address _platformFeeRecipient 23 | ) external; 24 | 25 | } 26 | 27 | interface IThirdwebNFT { 28 | 29 | function mintTo(address _to, string calldata _uri) external returns (uint256); 30 | 31 | } 32 | 33 | contract BenchmarkERC721ThirdwebLegacy is BenchmarkERC721Base { 34 | 35 | function deployContract( 36 | address deployerAddress, 37 | string memory name, 38 | string memory symbol, 39 | string memory contractURI 40 | ) external override returns (address contractAddress) { 41 | bytes memory encodedInitializer = abi.encodeCall( 42 | IBenchmarkERC721.initialize, 43 | ( 44 | deployerAddress, 45 | name, 46 | symbol, 47 | contractURI, 48 | new address[](0), 49 | deployerAddress, 50 | deployerAddress, 51 | 0, 52 | 0, 53 | deployerAddress 54 | ) 55 | ); 56 | 57 | contractAddress = IBenchmarkERC721(0x76F948E5F13B9A84A81E5681df8682BBf524805E).deployProxyByImplementation( 58 | 0xd534AC695ab818863FdE799afb2335F989C935e0, 59 | encodedInitializer, 60 | bytes32(0x3534313939303300000000000000000000000000000000000000000000000000) 61 | ); 62 | } 63 | 64 | function setupToken(address contractAddress) external override {} 65 | 66 | function mintToken(address contractAddress) external override { 67 | IThirdwebNFT(contractAddress).mintTo(tx.origin, "ipfs://QmVKEzCzn2wnakB33f2Zqhdnk5LrQiAKbuTA95bFcmKuUR/0"); 68 | } 69 | 70 | function mintBatchTokens(address contractAddress) external override {} 71 | 72 | function transferTokenFrom(address contractAddress) external override { 73 | IERC721(contractAddress).transferFrom(tx.origin, address(0xdead), 0); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /script/superchain/DeployTWCloneFactory.s.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | import {Script} from "forge-std/Script.sol"; 4 | import "lib/forge-std/src/console.sol"; 5 | import {TWCloneFactory} from "src/TWCloneFactory.sol"; 6 | import {SuperChainInterop} from "src/module/token/crosschain/SuperChainInterop.sol"; 7 | 8 | interface ICreateX { 9 | 10 | function deployCreate2(bytes32 salt, bytes memory initCode) external returns (address newContract); 11 | 12 | } 13 | 14 | contract DeployTWCloneFactoryScript is Script { 15 | 16 | address createX = 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed; 17 | 18 | function deployDeterministic(bytes32 salt, bytes memory creationCode) public returns (address) { 19 | address deployedAddress; 20 | 21 | // Deploy using CREATE2 22 | assembly { 23 | deployedAddress := create2(0, add(creationCode, 0x20), mload(creationCode), salt) 24 | } 25 | 26 | require(deployedAddress != address(0), "CREATE2 failed"); 27 | 28 | return deployedAddress; 29 | } 30 | 31 | function run() external { 32 | uint256 testPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); 33 | vm.startBroadcast(testPrivateKey); 34 | 35 | bytes32 salt; 36 | bytes memory initCode = abi.encodePacked(type(TWCloneFactory).creationCode); 37 | 38 | address twCloneFactory = ICreateX(createX).deployCreate2(salt, initCode); 39 | console.log("TWCloneFactory deployed: ", twCloneFactory); 40 | 41 | vm.stopBroadcast(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /script/superchain/IL2ToL2CrossDomainMessenger.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title IL2ToL2CrossDomainMessenger 5 | /// @notice Interface for the L2ToL2CrossDomainMessenger contract. 6 | interface IL2ToL2CrossDomainMessenger { 7 | 8 | /// @notice Mapping of message hashes to boolean receipt values. Note that a message will only 9 | /// be present in this mapping if it has successfully been relayed on this chain, and 10 | /// can therefore not be relayed again. 11 | /// @param _msgHash message hash to check. 12 | /// @return Returns true if the message corresponding to the `_msgHash` was successfully relayed. 13 | function successfulMessages(bytes32 _msgHash) external view returns (bool); 14 | 15 | /// @notice Retrieves the next message nonce. Message version will be added to the upper two 16 | /// bytes of the message nonce. Message version allows us to treat messages as having 17 | /// different structures. 18 | /// @return Nonce of the next message to be sent, with added message version. 19 | function messageNonce() external view returns (uint256); 20 | 21 | /// @notice Retrieves the sender of the current cross domain message. 22 | /// @return sender_ Address of the sender of the current cross domain message. 23 | function crossDomainMessageSender() external view returns (address sender_); 24 | 25 | /// @notice Retrieves the source of the current cross domain message. 26 | /// @return source_ Chain ID of the source of the current cross domain message. 27 | function crossDomainMessageSource() external view returns (uint256 source_); 28 | 29 | /// @notice Sends a message to some target address on a destination chain. Note that if the call 30 | /// always reverts, then the message will be unrelayable, and any ETH sent will be 31 | /// permanently locked. The same will occur if the target on the other chain is 32 | /// considered unsafe (see the _isUnsafeTarget() function). 33 | /// @param _destination Chain ID of the destination chain. 34 | /// @param _target Target contract or wallet address. 35 | /// @param _message Message to trigger the target address with. 36 | /// @return msgHash_ The hash of the message being sent, which can be used for tracking whether 37 | /// the message has successfully been relayed. 38 | function sendMessage(uint256 _destination, address _target, bytes calldata _message) 39 | external 40 | returns (bytes32 msgHash_); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /script/superchain/README.md: -------------------------------------------------------------------------------- 1 | # SuperChain ERC20 Module 2 | The contracts and scripts in this folder help setup and run the integration testing of the SuperChainInterop module with supersim 3 | 4 | #### IL2ToL2CrossDomainMessenger.sol 5 | Interface for the L2 to L2 cross domain messenger 6 | 7 | #### SuperChainBridge.sol 8 | Proof of concept SuperChain Bridge that contains `sendERC20` function and `relayERC20` function 9 | 10 | #### SuperChainERC20Setup.s.sol 11 | Script that deterministically deploys the `SuperChainBridge` and `SuperChainERC20` contract 12 | (which is composed of the `ERC20Core` and `SuperChainInterop` module) 13 | 14 | #### SuperChainInterop.s.sol 15 | Script that mints tokens on the L2 and calls `sendERC20` on the SuperChainBridge contract 16 | 17 | # How to run the integration test 18 | 1. Start supersim with the autorelayer enabled. 19 | ``` 20 | supersim --interop.autorelay 21 | ``` 22 | 23 | 2. Deploy the SuperChainBridge and SuperChainERC20 contracts on both chains 901 and 902 24 | Take note of the SuperChainBridge and Core addresses on both chains 25 | ``` 26 | # deploys to chain 901 27 | forge script --chain 901 script/superchain/SuperChainERC20Setup.s.sol:SuperChainERC20SetupScript --rpc-url http://127.0.0.1:9545 -vvvv --broadcast --evm-version cancun 28 | 29 | # deploys to chain 902 30 | forge script --chain 902 script/superchain/SuperChainERC20Setup.s.sol:SuperChainERC20SetupScript --rpc-url http://127.0.0.1:9546 -vvvv --broadcast --evm-version cancun 31 | ``` 32 | 33 | 3. Mint and sent tokens through the superchain 34 | ``` 35 | forge script --chain 901 script/superchain/SuperChainInterop.s.sol:SuperChainInteropScript --rpc-url http://127.0.0.1:9545 -vvvv --broadcast --evm-version cancun 36 | ``` 37 | 38 | 4. Check the balance of SuperChainERC20 for the test account 39 | ``` 40 | cast balance --erc20 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9546 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /script/superchain/SuperChainBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {IL2ToL2CrossDomainMessenger} from "./IL2ToL2CrossDomainMessenger.sol"; 5 | import "lib/forge-std/src/console.sol"; 6 | import {ICrossChainERC20} from "src/module/token/crosschain/SuperChainInterop.sol"; 7 | 8 | contract SuperChainBridge { 9 | 10 | address public immutable MESSENGER; 11 | 12 | event SentERC20(address indexed _token, address indexed, address indexed _to, uint256 _amount, uint256 _chainId); 13 | event RelayedERC20( 14 | address indexed _token, address indexed _from, address indexed _to, uint256 _amount, uint256 _source 15 | ); 16 | 17 | constructor(address _L2ToL2CrossDomainMesenger) { 18 | MESSENGER = _L2ToL2CrossDomainMesenger; 19 | } 20 | 21 | function sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId) 22 | external 23 | returns (bytes32 msgHash_) 24 | { 25 | ICrossChainERC20(_token).crosschainBurn(msg.sender, _amount); 26 | 27 | bytes memory _message = abi.encodeCall(this.relayERC20, (_token, msg.sender, _to, _amount)); 28 | msgHash_ = IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); 29 | console.log("message sent"); 30 | 31 | emit SentERC20(address(_token), msg.sender, _to, _amount, _chainId); 32 | } 33 | 34 | function relayERC20(address _token, address _from, address _to, uint256 _amount) external { 35 | require(msg.sender == MESSENGER); 36 | require(IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() == address(this)); 37 | 38 | uint256 _source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); 39 | console.log("message received"); 40 | 41 | ICrossChainERC20(_token).crosschainMint(_to, _amount); 42 | 43 | emit RelayedERC20(address(_token), _from, _to, _amount, _source); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /script/superchain/SuperChainERC20Setup.s.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | import {Script} from "forge-std/Script.sol"; 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {ERC20Core} from "src/core/token/ERC20Core.sol"; 7 | 8 | import {SuperChainBridge} from "./SuperChainBridge.sol"; 9 | import {SuperChainInterop} from "src/module/token/crosschain/SuperChainInterop.sol"; 10 | 11 | contract Core is ERC20Core { 12 | 13 | constructor(string memory name, string memory symbol, string memory contractURI, address owner) 14 | payable 15 | ERC20Core(name, symbol, contractURI, owner, new address[](0), new bytes[](0)) 16 | {} 17 | 18 | // disable mint callbacks for this script 19 | function _beforeMint(address to, uint256 amount, bytes calldata data) internal override {} 20 | 21 | } 22 | 23 | contract SuperChainERC20SetupScript is Script { 24 | 25 | SuperChainInterop public superchainInterop; 26 | address internal constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000023; 27 | 28 | function deployDeterministic(bytes32 salt, bytes memory creationCode, bytes memory encodedArgs) 29 | public 30 | returns (address) 31 | { 32 | address deployedAddress; 33 | 34 | bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, encodedArgs); 35 | 36 | // Deploy using CREATE2 37 | assembly { 38 | deployedAddress := create2(0, add(creationCodeWithArgs, 0x20), mload(creationCodeWithArgs), salt) 39 | } 40 | 41 | require(deployedAddress != address(0), "CREATE2 failed"); 42 | 43 | return deployedAddress; 44 | } 45 | 46 | function run() external { 47 | uint256 testPrivateKey = vm.envUint("TEST_PRIVATE_KEY"); 48 | address testAddress = vm.addr(testPrivateKey); 49 | vm.startBroadcast(testPrivateKey); 50 | 51 | bytes32 salt = keccak256("thirdweb"); 52 | 53 | // deploy superchain bridge 54 | address superchainBridge = 55 | deployDeterministic(salt, type(SuperChainBridge).creationCode, abi.encode(L2_TO_L2_CROSS_DOMAIN_MESSENGER)); 56 | console.log("SuperChainBridge deployed: ", address(superchainBridge)); 57 | 58 | // deploy core and superchainInterop module 59 | superchainInterop = new SuperChainInterop(); 60 | console.log("SuperChainInterop deployed: ", address(superchainInterop)); 61 | 62 | address core = deployDeterministic(salt, type(Core).creationCode, abi.encode("test", "TEST", "", testAddress)); 63 | console.log("Core deployed: ", core); 64 | 65 | // install module 66 | bytes memory encodedInstallParams = superchainInterop.encodeBytesOnInstall(address(superchainBridge)); 67 | Core(payable(core)).installModule(address(superchainInterop), encodedInstallParams); 68 | console.log("SuperChainInterop installed"); 69 | 70 | vm.stopBroadcast(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /script/superchain/SuperChainInterop.s.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | import {Script} from "forge-std/Script.sol"; 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {ERC20Core} from "src/core/token/ERC20Core.sol"; 7 | 8 | import {SuperChainBridge} from "./SuperChainBridge.sol"; 9 | import {SuperChainInterop} from "src/module/token/crosschain/SuperChainInterop.sol"; 10 | 11 | contract Core is ERC20Core { 12 | 13 | constructor(string memory name, string memory symbol, string memory contractURI, address owner) 14 | payable 15 | ERC20Core(name, symbol, contractURI, owner, new address[](0), new bytes[](0)) 16 | {} 17 | 18 | // disable mint callbacks for this script 19 | function _beforeMint(address to, uint256 amount, bytes calldata data) internal override {} 20 | 21 | } 22 | 23 | contract SuperChainInteropScript is Script { 24 | 25 | Core public core; 26 | SuperChainInterop public superchainInterop; 27 | SuperChainBridge public superchainBridge; 28 | address internal constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000023; 29 | 30 | function run() external { 31 | uint256 testPrivateKey = vm.envUint("TEST_PRIVATE_KEY"); 32 | address testAddress = vm.addr(testPrivateKey); 33 | vm.startBroadcast(testPrivateKey); 34 | 35 | // setup bridge and ERC-20 core 36 | superchainBridge = SuperChainBridge(0x6BF0d7B06930e016A9c03627c5BdE157cEfA1a47); 37 | core = Core(payable(0x6f7116d27F6fAE3986bdD05652EC22232B1DDAd7)); 38 | console.log("SuperChainERC20 and Bridge setup"); 39 | 40 | // mint tokens 41 | core.mint(testAddress, 10 ether, ""); 42 | console.log("Tokens minted to test address: ", core.balanceOf(testAddress)); 43 | 44 | // sendERC20 45 | superchainBridge.sendERC20(address(core), testAddress, 10 ether, 902); 46 | 47 | vm.stopBroadcast(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Module.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {IModule} from "./interface/IModule.sol"; 5 | 6 | abstract contract Module is IModule { 7 | 8 | function getModuleConfig() external pure virtual returns (ModuleConfig memory); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/TWCloneFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {LibClone} from "@solady/utils/LibClone.sol"; 5 | 6 | contract TWCloneFactory { 7 | 8 | /// @dev Emitted when a proxy is deployed. 9 | event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer, bytes data); 10 | 11 | /// @dev Deploys a proxy that points to the given implementation. 12 | function deployProxyByImplementation(address implementation, bytes memory data, bytes32 salt) 13 | public 14 | returns (address deployedProxy) 15 | { 16 | bytes32 saltHash = _guard(salt, data); 17 | deployedProxy = LibClone.cloneDeterministic(implementation, saltHash); 18 | 19 | emit ProxyDeployed(implementation, deployedProxy, msg.sender, data); 20 | 21 | if (data.length > 0) { 22 | // slither-disable-next-line unused-return 23 | (bool success,) = deployedProxy.call(data); 24 | require(success, "TWCloneFactory: proxy deployment failed"); 25 | } 26 | } 27 | 28 | function _guard(bytes32 salt, bytes memory data) internal view returns (bytes32) { 29 | // 01 if cross chain deployment is allowed 30 | // 00 if cross chain deployment is not allowed 31 | bool allowCrossChainDeployment = bytes1(salt[0]) == hex"01"; 32 | bool encodeDataIntoSalt = bytes1(salt[1]) == hex"01"; 33 | 34 | if (allowCrossChainDeployment && encodeDataIntoSalt) { 35 | return keccak256(abi.encode(salt, data)); 36 | } else if (allowCrossChainDeployment && !encodeDataIntoSalt) { 37 | return salt; 38 | } else if (!allowCrossChainDeployment && encodeDataIntoSalt) { 39 | return keccak256(abi.encode(salt, block.chainid, data)); 40 | } else { 41 | return keccak256(abi.encode(salt, block.chainid)); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/callback/AfterWithdrawCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Split} from "../libraries/Split.sol"; 5 | 6 | contract AfterWithdrawCallback { 7 | 8 | /*////////////////////////////////////////////////////////////// 9 | ERRORS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | error AfterWithdrawNotImplemented(); 13 | 14 | /*////////////////////////////////////////////////////////////// 15 | EXTERNAL FUNCTIONS 16 | //////////////////////////////////////////////////////////////*/ 17 | 18 | /** 19 | * @notice The afterWithdraw hook that is called by a core split fees contract after withdrawing tokens. 20 | * @dev Meant to be called by the core split fees contract. 21 | * @param amountToWithdraw The amount of tokens to withdraw. 22 | * @param account The address of the account to withdraw tokens to. 23 | * @param _token The address of the token to withdraw. 24 | */ 25 | function afterWithdraw(uint256 amountToWithdraw, address account, address _token) external virtual { 26 | revert AfterWithdrawNotImplemented(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/callback/BeforeApproveCallbackERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeApproveCallbackERC20 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeApproveCallbackERC20NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeApproveERC20 hook that is called by a core token before approving tokens. 18 | * 19 | * @param _from The address that is approving tokens. 20 | * @param _to The address that is being approved. 21 | * @param _amount The amount of tokens being approved. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function beforeApproveERC20(address _from, address _to, uint256 _amount) 25 | external 26 | virtual 27 | returns (bytes memory result) 28 | { 29 | revert BeforeApproveCallbackERC20NotImplemented(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/callback/BeforeApproveCallbackERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeApproveCallbackERC721 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeApproveCallbackERC721NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeApproveERC721 hook that is called by a core token before approving a token. 18 | * 19 | * @param _from The address that is approving tokens. 20 | * @param _to The address that is being approved. 21 | * @param _tokenId The token ID being approved. 22 | * @param _approve The approval status to set. 23 | * @return result Abi encoded bytes result of the hook. 24 | */ 25 | function beforeApproveERC721(address _from, address _to, uint256 _tokenId, bool _approve) 26 | external 27 | virtual 28 | returns (bytes memory result) 29 | { 30 | revert BeforeApproveCallbackERC721NotImplemented(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/callback/BeforeApproveForAllCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeApproveForAllCallback { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeApproveForAllCallbackNotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeApproveForAll hook that is called by a core token before approving an operator to transfer all tokens. 18 | * 19 | * @param _from The address that is approving tokens. 20 | * @param _to The address that is being approved. 21 | * @param _approved Whether to grant or revoke approval. 22 | */ 23 | function beforeApproveForAll(address _from, address _to, bool _approved) 24 | external 25 | virtual 26 | returns (bytes memory result) 27 | { 28 | revert BeforeApproveForAllCallbackNotImplemented(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/callback/BeforeBatchMintCallbackERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeBatchMintCallbackERC1155 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeBatchMintCallbackERC1155NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeBatchMintERC1155 hook that is called by a core token before minting tokens. 18 | * @param to The address to mint the token to. 19 | * @param ids The tokenIds to mint. 20 | * @param amounts The amounts of tokens to mint. 21 | * @param data ABI encoded data to pass to the beforeBatchMint hook. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function beforeBatchMintERC1155(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) 25 | external 26 | payable 27 | virtual 28 | returns (bytes memory result) 29 | { 30 | revert BeforeBatchMintCallbackERC1155NotImplemented(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/callback/BeforeBatchTransferCallbackERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeBatchTransferCallbackERC1155 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeBatchTransferCallbackERC1155NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeBatchTransferERC1155 hook that is called by a core token before batch transferring tokens. 18 | * 19 | * @param from The address that is transferring tokens. 20 | * @param to The address that is receiving tokens. 21 | * @param ids The token IDs being transferred. 22 | * @param values The quantities of tokens being transferred. 23 | * @return result Abi encoded bytes result of the hook. 24 | */ 25 | function beforeBatchTransferERC1155(address from, address to, uint256[] calldata ids, uint256[] calldata values) 26 | external 27 | virtual 28 | returns (bytes memory result) 29 | { 30 | revert BeforeBatchTransferCallbackERC1155NotImplemented(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/callback/BeforeBurnCallbackERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeBurnCallbackERC1155 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeBurnCallbackERC1155NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeBurnERC1155 hook that is called by a core token before burning a token. 18 | * 19 | * @param _from The address whose tokens are being burned. 20 | * @param _id The token ID being burned. 21 | * @param _value The quantity of tokens being burned. 22 | * @param _data The encoded arguments for the beforeBurn hook. 23 | * @return result Abi encoded bytes result of the hook. 24 | */ 25 | function beforeBurnERC1155(address _from, uint256 _id, uint256 _value, bytes memory _data) 26 | external 27 | payable 28 | virtual 29 | returns (bytes memory result) 30 | { 31 | revert BeforeBurnCallbackERC1155NotImplemented(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/callback/BeforeBurnCallbackERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeBurnCallbackERC20 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeBurnCallbackERC20NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeBurnERC20 hook that is called by a core token before burning tokens. 18 | * 19 | * @param _from The address whose tokens are being burned. 20 | * @param _amount The amount of tokens being burned. 21 | * @param _data The encoded arguments for the beforeBurn hook. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function beforeBurnERC20(address _from, uint256 _amount, bytes memory _data) 25 | external 26 | payable 27 | virtual 28 | returns (bytes memory result) 29 | { 30 | revert BeforeBurnCallbackERC20NotImplemented(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/callback/BeforeBurnCallbackERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeBurnCallbackERC721 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeBurnCallbackERC721NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeBurnERC721 hook that is called by a core token before burning a token. 18 | * 19 | * @param _tokenId The token ID being burned. 20 | * @param _data The encoded arguments for the beforeBurn hook. 21 | * @return result Abi encoded bytes result of the hook. 22 | */ 23 | function beforeBurnERC721(uint256 _tokenId, bytes memory _data) 24 | external 25 | payable 26 | virtual 27 | returns (bytes memory result) 28 | { 29 | revert BeforeBurnCallbackERC721NotImplemented(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/callback/BeforeDistributeCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Split} from "../libraries/Split.sol"; 5 | 6 | contract BeforeDistributeCallback { 7 | 8 | /*////////////////////////////////////////////////////////////// 9 | ERRORS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | error BeforeDistributeNotImplemented(); 13 | 14 | /*////////////////////////////////////////////////////////////// 15 | EXTERNAL FUNCTIONS 16 | //////////////////////////////////////////////////////////////*/ 17 | 18 | /** 19 | * @notice The beforeDistribute hook that is called by a core split fees contract before distributing tokens. 20 | * @dev Meant to be called by the core split fees contract. 21 | * @param _splitWallet The address of the split wallet contract. 22 | * @param _token The address of the token to distribute. 23 | * @return amountToSplit The amount of tokens to distribute. 24 | */ 25 | function beforeDistribute(address _splitWallet, address _token) external virtual returns (uint256, Split memory) { 26 | revert BeforeDistributeNotImplemented(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/callback/BeforeMintCallbackERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeMintCallbackERC1155 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeMintCallbackERC1155NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeMintERC1155 hook that is called by a core token before minting tokens. 18 | * 19 | * @param _to The address that is minting tokens. 20 | * @param _id The token ID being minted. 21 | * @param _amount The amount of tokens to mint. 22 | * @param _data Optional extra data passed to the hook. 23 | * @return result Abi encoded bytes result of the hook. 24 | */ 25 | function beforeMintERC1155(address _to, uint256 _id, uint256 _amount, bytes memory _data) 26 | external 27 | payable 28 | virtual 29 | returns (bytes memory result) 30 | { 31 | revert BeforeMintCallbackERC1155NotImplemented(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/callback/BeforeMintCallbackERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeMintCallbackERC20 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeMintCallbackERC20NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeMintERC20 hook that is called by a core token before minting tokens. 18 | * 19 | * @param _to The address to mint tokens to. 20 | * @param _amount The amount of tokens to mint. 21 | * @param _data Optional extra data passed to the hook. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function beforeMintERC20(address _to, uint256 _amount, bytes memory _data) 25 | external 26 | payable 27 | virtual 28 | returns (bytes memory result) 29 | { 30 | revert BeforeMintCallbackERC20NotImplemented(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/callback/BeforeMintCallbackERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeMintCallbackERC721 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeMintCallbackERC721NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeMintERC721 hook that is called by a core token before minting tokens. 18 | * 19 | * @param _to The address that is minting tokens. 20 | * @param _startTokenId The token ID being minted. 21 | * @param _amount The amount of tokens to mint. 22 | * @param _data Optional extra data passed to the hook. 23 | * @return result Abi encoded bytes result of the hook. 24 | */ 25 | function beforeMintERC721(address _to, uint256 _startTokenId, uint256 _amount, bytes memory _data) 26 | external 27 | payable 28 | virtual 29 | returns (bytes memory result) 30 | { 31 | revert BeforeMintCallbackERC721NotImplemented(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/callback/BeforeMintWithSignatureCallbackERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeMintWithSignatureCallbackERC1155 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeMintWithSignatureCallbackERC1155NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeMintWithSignatureERC1155 hook that is called by a core token before minting tokens. 18 | * 19 | * @param _to The address that is minting tokens. 20 | * @param _id The token ID being minted. 21 | * @param _amount The quantity of tokens to mint. 22 | * @param _data Optional extra data passed to the hook. 23 | * @param _signer The address that signed the minting request. 24 | * @return result Abi encoded bytes result of the hook. 25 | */ 26 | function beforeMintWithSignatureERC1155( 27 | address _to, 28 | uint256 _id, 29 | uint256 _amount, 30 | bytes memory _data, 31 | address _signer 32 | ) external payable virtual returns (bytes memory result) { 33 | revert BeforeMintWithSignatureCallbackERC1155NotImplemented(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/callback/BeforeMintWithSignatureCallbackERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeMintWithSignatureCallbackERC20 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeMintWithSignatureCallbackERC20NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeMintWithSignatureERC20 hook that is called by a core token before minting tokens. 18 | * 19 | * @param _to The address that is minting tokens. 20 | * @param _amount The amount of tokens to mint. 21 | * @param _data Optional extra data passed to the hook. 22 | * @param _signer The address that signed the minting request. 23 | * @return result Abi encoded bytes result of the hook. 24 | */ 25 | function beforeMintWithSignatureERC20(address _to, uint256 _amount, bytes memory _data, address _signer) 26 | external 27 | payable 28 | virtual 29 | returns (bytes memory result) 30 | { 31 | revert BeforeMintWithSignatureCallbackERC20NotImplemented(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/callback/BeforeMintWithSignatureCallbackERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeMintWithSignatureCallbackERC721 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeMintWithSignatureCallbackERC721NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeMintWithSignatureERC721 hook that is called by a core token before minting tokens. 18 | * 19 | * @param _to The address that is minting tokens. 20 | * @param _startTokenId The token ID being minted. 21 | * @param _amount The amount of tokens to mint. 22 | * @param _data Optional extra data passed to the hook. 23 | * @param _signer The address that signed the minting request. 24 | * @return result Abi encoded bytes result of the hook. 25 | */ 26 | function beforeMintWithSignatureERC721( 27 | address _to, 28 | uint256 _startTokenId, 29 | uint256 _amount, 30 | bytes memory _data, 31 | address _signer 32 | ) external payable virtual returns (bytes memory result) { 33 | revert BeforeMintWithSignatureCallbackERC721NotImplemented(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/callback/BeforeTransferCallbackERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeTransferCallbackERC1155 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeTransferCallbackERC1155NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeTransferERC1155 hook that is called by a core token before transferring a token. 18 | * @param _from The address that is transferring tokens. 19 | * @param _to The address that is receiving tokens. 20 | * @param _id The token ID being transferred. 21 | * @param _amount The amount of tokens being transferred. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function beforeTransferERC1155(address _from, address _to, uint256 _id, uint256 _amount) 25 | external 26 | virtual 27 | returns (bytes memory result) 28 | { 29 | revert BeforeTransferCallbackERC1155NotImplemented(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/callback/BeforeTransferCallbackERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeTransferCallbackERC20 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeTransferCallbackERC20NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeTransferERC20 hook that is called by a core token before transferring tokens. 18 | * 19 | * @param _from The address that is transferring tokens. 20 | * @param _to The address that is receiving tokens. 21 | * @param _amount The amount of tokens being transferred. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function beforeTransferERC20(address _from, address _to, uint256 _amount) 25 | external 26 | virtual 27 | returns (bytes memory result) 28 | { 29 | revert BeforeTransferCallbackERC20NotImplemented(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/callback/BeforeTransferCallbackERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract BeforeTransferCallbackERC721 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error BeforeTransferCallbackERC721NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeTransferERC721 hook that is called by a core token before transferring a token. 18 | * 19 | * @param _from The address that is transferring tokens. 20 | * @param _to The address that is receiving tokens. 21 | * @param _tokenId The token ID being transferred. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function beforeTransferERC721(address _from, address _to, uint256 _tokenId) 25 | external 26 | virtual 27 | returns (bytes memory result) 28 | { 29 | revert BeforeTransferCallbackERC721NotImplemented(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/callback/OnTokenURICallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract OnTokenURICallback { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error OnTokenURICallbackNotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice Returns the URI to fetch token metadata from. 18 | * @dev Meant to be called by the core token contract. 19 | * @param _tokenId The token ID of the NFT. 20 | * @return metadata The URI to fetch token metadata from. 21 | */ 22 | function onTokenURI(uint256 _tokenId) external view virtual returns (string memory metadata) { 23 | revert OnTokenURICallbackNotImplemented(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/callback/UpdateMetadataCallbackERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract UpdateMetadataCallbackERC1155 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error UpdateMetadataCallbackERC1155NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeMintERC1155 hook that is called by a core token before minting tokens. 18 | * 19 | * @param _to The address that is minting tokens. 20 | * @param _quantity The quantity of tokens to mint. 21 | * @param _baseURI The URI to fetch token metadata from. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function updateMetadataERC1155(address _to, uint256 _startTokenId, uint256 _quantity, string calldata _baseURI) 25 | external 26 | payable 27 | virtual 28 | returns (bytes memory result) 29 | { 30 | revert UpdateMetadataCallbackERC1155NotImplemented(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/callback/UpdateMetadataCallbackERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract UpdateMetadataCallbackERC721 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error UpdateMetadataCallbackERC721NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The beforeMintERC721 hook that is called by a core token before minting tokens. 18 | * 19 | * @param _to The address that is minting tokens. 20 | * @param _amount The amount of tokens to mint. 21 | * @param _baseURI The URI to fetch token metadata from. 22 | * @return result Abi encoded bytes result of the hook. 23 | */ 24 | function updateMetadataERC721(address _to, uint256 _startTokenId, uint256 _amount, string calldata _baseURI) 25 | external 26 | payable 27 | virtual 28 | returns (bytes memory result) 29 | { 30 | revert UpdateMetadataCallbackERC721NotImplemented(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/callback/UpdateTokenIdERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract UpdateTokenIdCallbackERC1155 { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error UpdateTokenIdCallbackERC1155NotImplemented(); 11 | 12 | /*////////////////////////////////////////////////////////////// 13 | EXTERNAL FUNCTIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice The updateTokenIdERC1155 hook that is called by a core token before minting tokens. 18 | * 19 | * @dev If the tokenId is type(uint256).max, the next tokenId will be set to the current next tokenId + amount. 20 | * 21 | * @param _tokenId The tokenId to mint. 22 | * @return result tokenId to mint. 23 | */ 24 | function updateTokenIdERC1155(uint256 _tokenId) external payable virtual returns (uint256) { 25 | revert UpdateTokenIdCallbackERC1155NotImplemented(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/core/MintFeeManagerCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Core} from "../Core.sol"; 5 | 6 | contract MintFeeManagerCore is Core { 7 | 8 | constructor(address _owner, address[] memory _modules, bytes[] memory _moduleInstallData) { 9 | _initializeOwner(_owner); 10 | 11 | // Install and initialize modules 12 | require(_modules.length == _moduleInstallData.length); 13 | for (uint256 i = 0; i < _modules.length; i++) { 14 | _installModule(_modules[i], _moduleInstallData[i]); 15 | } 16 | } 17 | 18 | function getSupportedCallbackFunctions() 19 | public 20 | pure 21 | override 22 | returns (SupportedCallbackFunction[] memory supportedCallbackFunctions) 23 | { 24 | supportedCallbackFunctions = new SupportedCallbackFunction[](0); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/core/SplitFeesCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {SplitWallet} from "../core/SplitWallet.sol"; 5 | 6 | import {IERC20Metadata} from "../interface/IERC20Metadata.sol"; 7 | import {Cast} from "../libraries/Cast.sol"; 8 | import {ShortString, ShortStrings} from "../libraries/ShortString.sol"; 9 | import {Split} from "../libraries/Split.sol"; 10 | 11 | import {AfterWithdrawCallback} from "../callback/AfterWithdrawCallback.sol"; 12 | import {BeforeDistributeCallback} from "../callback/BeforeDistributeCallback.sol"; 13 | 14 | import {ERC6909} from "@solady/tokens/ERC6909.sol"; 15 | import {LibClone} from "@solady/utils/LibClone.sol"; 16 | import {Multicallable} from "@solady/utils/Multicallable.sol"; 17 | 18 | import {Core} from "../Core.sol"; 19 | 20 | contract SplitFeesCore is Core, Multicallable, ERC6909 { 21 | 22 | using ShortStrings for string; 23 | using ShortStrings for ShortString; 24 | using Cast for uint256; 25 | using Cast for address; 26 | 27 | /*////////////////////////////////////////////////////////////// 28 | CONSTANTS 29 | //////////////////////////////////////////////////////////////*/ 30 | 31 | /// @notice prefix for metadata name. 32 | string private constant METADATA_PREFIX_NAME = "Splits Wrapped "; 33 | 34 | /// @notice prefix for metadata symbol. 35 | string private constant METADATA_PREFIX_SYMBOL = "splits"; 36 | 37 | /// @notice address of the native token, inline with ERC 7528. 38 | address public constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; 39 | 40 | /// @notice uint256 representation of the native token. 41 | uint256 public constant NATIVE_TOKEN_ID = uint256(uint160(NATIVE_TOKEN_ADDRESS)); 42 | 43 | /// @notice metadata name of the native token. 44 | ShortString private immutable NATIVE_TOKEN_NAME; 45 | 46 | /// @notice metadata symbol of the native token. 47 | ShortString private immutable NATIVE_TOKEN_SYMBOL; 48 | 49 | address public splitWalletImplementation; 50 | 51 | /*////////////////////////////////////////////////////////////// 52 | CONSTRUCTOR 53 | //////////////////////////////////////////////////////////////*/ 54 | 55 | receive() external payable {} 56 | 57 | constructor(address _owner, address[] memory _modules, bytes[] memory _moduleInstallData) { 58 | _initializeOwner(_owner); 59 | 60 | splitWalletImplementation = address(new SplitWallet(_owner)); 61 | 62 | // Install and initialize modules 63 | require(_modules.length == _moduleInstallData.length); 64 | for (uint256 i = 0; i < _modules.length; i++) { 65 | _installModule(_modules[i], _moduleInstallData[i]); 66 | } 67 | } 68 | 69 | function getSupportedCallbackFunctions() 70 | public 71 | pure 72 | override 73 | returns (SupportedCallbackFunction[] memory supportedCallbackFunctions) 74 | { 75 | supportedCallbackFunctions = new SupportedCallbackFunction[](2); 76 | 77 | supportedCallbackFunctions[0] = SupportedCallbackFunction({ 78 | selector: BeforeDistributeCallback.beforeDistribute.selector, 79 | mode: CallbackMode.REQUIRED 80 | }); 81 | supportedCallbackFunctions[1] = SupportedCallbackFunction({ 82 | selector: AfterWithdrawCallback.afterWithdraw.selector, 83 | mode: CallbackMode.REQUIRED 84 | }); 85 | } 86 | 87 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC6909, Core) returns (bool) { 88 | return interfaceId == 0x0f632fb3 || super.supportsInterface(interfaceId); 89 | } 90 | 91 | function distribute(address _splitWallet, address _token) external { 92 | (uint256 amountToSplit, Split memory _split) = _beforeDistribute(_splitWallet, _token); 93 | 94 | uint256 length = _split.recipients.length; 95 | for (uint256 i = 0; i < length; i++) { 96 | uint256 amountToSend = (amountToSplit * _split.allocations[i]) / _split.totalAllocation; 97 | 98 | _mint(_split.recipients[i], _token.toUint256(), amountToSend); 99 | } 100 | } 101 | 102 | function withdraw(address account, address _token) external { 103 | uint256 amountToWithdraw = balanceOf(account, _token.toUint256()); 104 | _burn(account, _token.toUint256(), amountToWithdraw); 105 | _afterWithdraw(amountToWithdraw, account, _token); 106 | } 107 | 108 | /** 109 | * @notice Name of a given token. 110 | * @param id The id of the token. 111 | * @return The name of the token. 112 | */ 113 | function name(uint256 id) public view override returns (string memory) { 114 | if (id == NATIVE_TOKEN_ID) { 115 | return NATIVE_TOKEN_NAME.toString(); 116 | } 117 | return string.concat(METADATA_PREFIX_NAME, IERC20Metadata(id.toAddress()).name()); 118 | } 119 | 120 | /** 121 | * @notice Symbol of a given token. 122 | * @param id The id of the token. 123 | * @return The symbol of the token. 124 | */ 125 | function symbol(uint256 id) public view override returns (string memory) { 126 | if (id == NATIVE_TOKEN_ID) { 127 | return NATIVE_TOKEN_SYMBOL.toString(); 128 | } 129 | return string.concat(METADATA_PREFIX_SYMBOL, IERC20Metadata(id.toAddress()).symbol()); 130 | } 131 | 132 | /// @dev Returns the Uniform Resource Identifier (URI) for token `id`. 133 | function tokenURI(uint256 id) public view override returns (string memory) { 134 | return ""; 135 | } 136 | 137 | /// @dev Fetches token URI from the token metadata hook. 138 | function _beforeDistribute(address _splitWallet, address _token) 139 | internal 140 | returns (uint256 amountToSplit, Split memory _split) 141 | { 142 | (, bytes memory returndata) = _executeCallbackFunction( 143 | BeforeDistributeCallback.beforeDistribute.selector, 144 | abi.encodeCall(BeforeDistributeCallback.beforeDistribute, (_splitWallet, _token)) 145 | ); 146 | (amountToSplit, _split) = abi.decode(returndata, (uint256, Split)); 147 | } 148 | 149 | /// @dev Fetches token URI from the token metadata hook. 150 | function _afterWithdraw(uint256 amountToWithdraw, address account, address _token) internal { 151 | _executeCallbackFunction( 152 | AfterWithdrawCallback.afterWithdraw.selector, 153 | abi.encodeCall(AfterWithdrawCallback.afterWithdraw, (amountToWithdraw, account, _token)) 154 | ); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/core/SplitWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Core} from "../Core.sol"; 5 | 6 | import {IERC20} from "../interface/IERC20.sol"; 7 | import {Ownable} from "@solady/auth/Ownable.sol"; 8 | 9 | contract SplitWallet is Ownable { 10 | 11 | address public immutable splitFees; 12 | 13 | constructor(address _owner) { 14 | splitFees = msg.sender; 15 | _initializeOwner(_owner); 16 | } 17 | 18 | error OnlySplitFees(); 19 | 20 | modifier onlySplitFees() { 21 | if (msg.sender != splitFees) { 22 | revert OnlySplitFees(); 23 | } 24 | _; 25 | } 26 | 27 | function transferETH(uint256 amount) external payable onlySplitFees { 28 | (bool success,) = splitFees.call{value: amount}(""); 29 | require(success, "Failed to send Ether"); 30 | } 31 | 32 | function transferERC20(address token, uint256 amount) external onlySplitFees { 33 | IERC20(token).transfer(splitFees, amount); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/core/token/ERC1155Core.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC1155Base} from "./ERC1155Base.sol"; 5 | 6 | contract ERC1155Core is ERC1155Base { 7 | 8 | constructor( 9 | string memory _name, 10 | string memory _symbol, 11 | string memory _contractURI, 12 | address _owner, 13 | address[] memory _modules, 14 | bytes[] memory _moduleInstallData 15 | ) { 16 | _initialize(_name, _symbol, _contractURI, _owner, _modules, _moduleInstallData); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/core/token/ERC1155CoreInitializable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC1155Base} from "./ERC1155Base.sol"; 5 | import {Initializable} from "@solady/utils/Initializable.sol"; 6 | 7 | contract ERC1155CoreInitializable is ERC1155Base, Initializable { 8 | 9 | constructor() { 10 | _disableInitializers(); 11 | } 12 | 13 | function initialize( 14 | string memory _name, 15 | string memory _symbol, 16 | string memory _contractURI, 17 | address _owner, 18 | address[] memory _modules, 19 | bytes[] memory _moduleInstallData 20 | ) external payable initializer { 21 | _initialize(_name, _symbol, _contractURI, _owner, _modules, _moduleInstallData); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/core/token/ERC20Core.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC20Base} from "./ERC20Base.sol"; 5 | 6 | contract ERC20Core is ERC20Base { 7 | 8 | constructor( 9 | string memory _name, 10 | string memory _symbol, 11 | string memory _contractURI, 12 | address _owner, 13 | address[] memory _modules, 14 | bytes[] memory _moduleInstallData 15 | ) { 16 | _initialize(_name, _symbol, _contractURI, _owner, _modules, _moduleInstallData); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/core/token/ERC20CoreInitializable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC20Base} from "./ERC20Base.sol"; 5 | import {Initializable} from "@solady/utils/Initializable.sol"; 6 | 7 | contract ERC20CoreInitializable is ERC20Base, Initializable { 8 | 9 | constructor() { 10 | _disableInitializers(); 11 | } 12 | 13 | function initialize( 14 | string memory _name, 15 | string memory _symbol, 16 | string memory _contractURI, 17 | address _owner, 18 | address[] memory _modules, 19 | bytes[] memory _moduleInstallData 20 | ) external payable initializer { 21 | _initialize(_name, _symbol, _contractURI, _owner, _modules, _moduleInstallData); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/core/token/ERC721Core.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC721Base} from "./ERC721Base.sol"; 5 | 6 | contract ERC721Core is ERC721Base { 7 | 8 | constructor( 9 | string memory _name, 10 | string memory _symbol, 11 | string memory _contractURI, 12 | address _owner, 13 | address[] memory _modules, 14 | bytes[] memory _moduleInstallData 15 | ) { 16 | _initialize(_name, _symbol, _contractURI, _owner, _modules, _moduleInstallData); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/core/token/ERC721CoreInitializable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC721Base} from "./ERC721Base.sol"; 5 | import {Initializable} from "@solady/utils/Initializable.sol"; 6 | 7 | contract ERC721CoreInitializable is ERC721Base, Initializable { 8 | 9 | constructor() { 10 | _disableInitializers(); 11 | } 12 | 13 | function initialize( 14 | string memory _name, 15 | string memory _symbol, 16 | string memory _contractURI, 17 | address _owner, 18 | address[] memory _modules, 19 | bytes[] memory _moduleInstallData 20 | ) external payable initializer { 21 | _initialize(_name, _symbol, _contractURI, _owner, _modules, _moduleInstallData); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/interface/ICore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {IERC165} from "./IERC165.sol"; 5 | import {IModuleConfig} from "./IModuleConfig.sol"; 6 | 7 | interface ICore is IModuleConfig, IERC165 { 8 | 9 | /*////////////////////////////////////////////////////////////// 10 | STRUCTS & ENUMS 11 | //////////////////////////////////////////////////////////////*/ 12 | 13 | /** 14 | * @dev Whether execution reverts when the callback function is not implemented by any installed Module. 15 | * @param OPTIONAL Execution does not revert when the callback function is not implemented. 16 | * @param REQUIRED Execution reverts when the callback function is not implemented. 17 | */ 18 | enum CallbackMode { 19 | OPTIONAL, 20 | REQUIRED 21 | } 22 | 23 | /** 24 | * @dev Struct representing a callback function called on an Module during some fixed function's execution. 25 | * @param selector The 4-byte function selector of the callback function. 26 | * @param mode Whether execution reverts when the callback function is not implemented by any installed Module. 27 | */ 28 | struct SupportedCallbackFunction { 29 | bytes4 selector; 30 | CallbackMode mode; 31 | } 32 | 33 | /** 34 | * @dev Struct representing an installed Module. 35 | * @param implementation The address of the Module contract. 36 | * @param config The Module Config of the Module contract. 37 | */ 38 | struct InstalledModule { 39 | address implementation; 40 | ModuleConfig config; 41 | } 42 | 43 | /*////////////////////////////////////////////////////////////// 44 | VIEW FUNCTIONS 45 | //////////////////////////////////////////////////////////////*/ 46 | 47 | /// @dev Returns all callback function calls made to Modules at some point during a fixed function's execution. 48 | function getSupportedCallbackFunctions() external pure returns (SupportedCallbackFunction[] memory); 49 | 50 | /// @dev Returns all installed modules and their respective module configs. 51 | function getInstalledModules() external view returns (InstalledModule[] memory); 52 | 53 | /*////////////////////////////////////////////////////////////// 54 | EXTERNAL FUNCTIONS 55 | //////////////////////////////////////////////////////////////*/ 56 | 57 | /** 58 | * @dev Installs an Module in the Core. 59 | * 60 | * @param moduleContract The address of the Module contract to be installed. 61 | * @param data The data to be passed to the Module's onInstall callback function. 62 | * 63 | * MUST implement authorization control. 64 | * MUST call `onInstall` callback function if Module Config has registerd for installation callbacks. 65 | * MUST revert if Core does not implement the interface required by the Module, specified in the Module Config. 66 | * MUST revert if any callback or fallback function in the Module's ModuleConfig is already registered in the Core with another Module. 67 | * 68 | * MAY interpret the provided address as the implementation address of the Module contract to install as a proxy. 69 | */ 70 | function installModule(address moduleContract, bytes calldata data) external payable; 71 | 72 | /** 73 | * @dev Uninstalls an Module from the Core. 74 | * 75 | * @param moduleContract The address of the Module contract to be uninstalled. 76 | * @param data The data to be passed to the Module's onUninstall callback function. 77 | * 78 | * MUST implement authorization control. 79 | * MUST call `onUninstall` callback function if Module Config has registerd for installation callbacks. 80 | * 81 | * MAY interpret the provided address as the implementation address of the Module contract which is installed as a proxy. 82 | */ 83 | function uninstallModule(address moduleContract, bytes calldata data) external payable; 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/interface/IERC165.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface IERC165 { 5 | 6 | /// @notice Query if a contract implements an interface 7 | /// @param interfaceID The interface identifier, as specified in ERC-165 8 | /// @dev Interface identification is specified in ERC-165. This function 9 | /// uses less than 30,000 gas. 10 | /// @return `true` if the contract implements `interfaceID` and 11 | /// `interfaceID` is not 0xffffffff, `false` otherwise 12 | function supportsInterface(bytes4 interfaceID) external view returns (bool); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/interface/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface IERC20 { 5 | 6 | event Transfer(address indexed from, address indexed to, uint256 value); 7 | event Approval(address indexed owner, address indexed spender, uint256 value); 8 | 9 | function totalSupply() external view returns (uint256); 10 | 11 | function balanceOf(address account) external view returns (uint256); 12 | 13 | function transfer(address to, uint256 value) external returns (bool); 14 | 15 | function allowance(address owner, address spender) external view returns (uint256); 16 | 17 | function approve(address spender, uint256 value) external returns (bool); 18 | 19 | function transferFrom(address from, address to, uint256 value) external returns (bool); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/interface/IERC20Metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: APACHE-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "./IERC20.sol"; 5 | 6 | interface IERC20Metadata is IERC20 { 7 | 8 | function name() external view returns (string memory); 9 | 10 | function symbol() external view returns (string memory); 11 | 12 | function decimals() external view returns (uint8); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/interface/IERC7572.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface IERC7572 { 5 | 6 | function contractURI() external view returns (string memory); 7 | 8 | event ContractURIUpdated(); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/interface/IInstallationCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface IInstallationCallback { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | EXTERNAL FUNCTIONS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | /** 11 | * @dev Called by a Core into an Module during the installation of the Module. 12 | * 13 | * @param data The data passed to the Core's installModule function. 14 | */ 15 | function onInstall(bytes calldata data) external; 16 | 17 | /** 18 | * @dev Called by a Core into an Module during the uninstallation of the Module. 19 | * 20 | * @param data The data passed to the Core's uninstallModule function. 21 | */ 22 | function onUninstall(bytes calldata data) external; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/interface/IMintFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface IMintFeeManager { 5 | 6 | function calculatePlatformFeeAndRecipient(uint256 _price) 7 | external 8 | view 9 | returns (uint256 platformFee, address feeRecipient); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/interface/IModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {IModuleConfig} from "./IModuleConfig.sol"; 5 | 6 | interface IModule is IModuleConfig { 7 | 8 | /*////////////////////////////////////////////////////////////// 9 | VIEW FUNCTIONS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | /** 13 | * @dev Returns the ModuleConfig of the Module contract. 14 | */ 15 | function getModuleConfig() external pure returns (ModuleConfig memory); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/interface/IModuleConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface IModuleConfig { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | STRUCTS & ENUMS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | /** 11 | * @dev Struct for a callback function. Called by a Core into an Module during the execution of some fixed function. 12 | * 13 | * @param selector The 4-byte selector of the function. 14 | * @param callType The type of call to be made to the function. 15 | */ 16 | struct CallbackFunction { 17 | bytes4 selector; 18 | } 19 | 20 | /** 21 | * @dev Struct for a fallback function. Called by a Core into an Module via the Core's fallback. 22 | * 23 | * @param selector The 4-byte selector of the function. 24 | * @param callType The type of call to be made to the function. 25 | * @param permissionBits Core’s fallback function MUST check that msg.sender has these permissions before 26 | * performing a call on the Module. (OPTIONAL field) 27 | */ 28 | struct FallbackFunction { 29 | bytes4 selector; 30 | uint256 permissionBits; 31 | } 32 | 33 | /** 34 | * @dev Struct containing all information that a Core uses to check whether an Module is compatible for installation. 35 | * 36 | * @param registerInstallationCallback Whether the Module expects onInstall and onUninstall callback function calls at 37 | * installation and uninstallation time, respectively 38 | * @param requiredInterfaces The ERC-165 interface that a Core MUST support to be compatible for installation. OPTIONAL -- can be bytes4(0) 39 | * if there is no required interface id. 40 | * @param supportedInterfaces The ERC-165 interfaces that a Core supports upon installing the Module. 41 | * @param callbackFunctions List of callback functions that the Core MUST call at some point in the execution of its fixed functions. 42 | * @param fallbackFunctions List of functions that the Core MUST call via its fallback function with the Module as the call destination. 43 | */ 44 | struct ModuleConfig { 45 | bool registerInstallationCallback; 46 | bytes4[] requiredInterfaces; 47 | bytes4[] supportedInterfaces; 48 | CallbackFunction[] callbackFunctions; 49 | FallbackFunction[] fallbackFunctions; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/interface/ISplitWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | interface ISplitWallet { 5 | 6 | function transferETH(uint256 amount) external payable; 7 | function transferERC20(address token, uint256 amount) external; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/libraries/Cast.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | library Cast { 4 | 5 | function toAddress(uint256 _value) internal pure returns (address) { 6 | return address(uint160(_value)); 7 | } 8 | 9 | function toUint256(address _value) internal pure returns (uint256) { 10 | return uint256(uint160(_value)); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/libraries/ShortString.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | type ShortString is bytes32; 4 | 5 | library ShortStrings { 6 | 7 | error StringTooLong(string str); 8 | error InvalidShortString(); 9 | 10 | /** 11 | * @dev Encode a string of at most 31 chars into a `ShortString`. 12 | * 13 | * This will trigger a `StringTooLong` error is the input string is too long. 14 | */ 15 | function toShortString(string memory str) internal pure returns (ShortString) { 16 | bytes memory bstr = bytes(str); 17 | if (bstr.length > 31) { 18 | revert StringTooLong(str); 19 | } 20 | return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length)); 21 | } 22 | 23 | /** 24 | * @dev Decode a `ShortString` back to a "normal" string. 25 | */ 26 | function toString(ShortString sstr) internal pure returns (string memory) { 27 | uint256 len = byteLength(sstr); 28 | // using `new string(len)` would work locally but is not memory safe. 29 | string memory str = new string(32); 30 | /// @solidity memory-safe-assembly 31 | assembly { 32 | mstore(str, len) 33 | mstore(add(str, 0x20), sstr) 34 | } 35 | return str; 36 | } 37 | 38 | /** 39 | * @dev Return the length of a `ShortString`. 40 | */ 41 | function byteLength(ShortString sstr) internal pure returns (uint256) { 42 | uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF; 43 | if (result > 31) { 44 | revert InvalidShortString(); 45 | } 46 | return result; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/libraries/Split.sol: -------------------------------------------------------------------------------- 1 | struct Split { 2 | address controller; 3 | address[] recipients; 4 | uint256[] allocations; 5 | uint256 totalAllocation; 6 | } 7 | -------------------------------------------------------------------------------- /src/module/MintFeeManagerModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Module} from "../Module.sol"; 5 | import {Role} from "../Role.sol"; 6 | 7 | library MintFeeManagerStorage { 8 | 9 | /// @custom:storage-location erc7201:mint.fee.manager 10 | bytes32 public constant MINT_FEE_MANAGER_STORAGE_POSITION = 11 | keccak256(abi.encode(uint256(keccak256("mint.fee.manager")) - 1)) & ~bytes32(uint256(0xff)); 12 | 13 | struct Data { 14 | address feeRecipient; 15 | uint256 defaultMintFee; 16 | mapping(address _contract => uint256 mintFee) mintFees; 17 | } 18 | 19 | function data() internal pure returns (Data storage data_) { 20 | bytes32 position = MINT_FEE_MANAGER_STORAGE_POSITION; 21 | assembly { 22 | data_.slot := position 23 | } 24 | } 25 | 26 | } 27 | 28 | contract MintFeeManagerModule is Module { 29 | 30 | uint256 private constant MINT_FEE_MAX_BPS = 10_000; 31 | 32 | event MintFeeUpdated(address indexed contractAddress, uint256 mintFee); 33 | event DefaultMintFeeUpdated(uint256 mintFee); 34 | event feeRecipientUpdated(address feeRecipient); 35 | 36 | error MintFeeExceedsMaxBps(); 37 | 38 | function getModuleConfig() external pure override returns (ModuleConfig memory config) { 39 | config.fallbackFunctions = new FallbackFunction[](7); 40 | 41 | config.fallbackFunctions[0] = FallbackFunction({selector: this.getfeeRecipient.selector, permissionBits: 0}); 42 | config.fallbackFunctions[1] = 43 | FallbackFunction({selector: this.setfeeRecipient.selector, permissionBits: Role._MANAGER_ROLE}); 44 | config.fallbackFunctions[2] = FallbackFunction({selector: this.getDefaultMintFee.selector, permissionBits: 0}); 45 | config.fallbackFunctions[3] = 46 | FallbackFunction({selector: this.setDefaultMintFee.selector, permissionBits: Role._MANAGER_ROLE}); 47 | config.fallbackFunctions[4] = FallbackFunction({selector: this.getMintFees.selector, permissionBits: 0}); 48 | config.fallbackFunctions[5] = 49 | FallbackFunction({selector: this.updateMintFee.selector, permissionBits: Role._MANAGER_ROLE}); 50 | config.fallbackFunctions[6] = 51 | FallbackFunction({selector: this.calculatePlatformFeeAndRecipient.selector, permissionBits: 0}); 52 | 53 | config.registerInstallationCallback = true; 54 | } 55 | 56 | /// @dev Called by a Core into an Module during the installation of the Module. 57 | function onInstall(bytes calldata data) external { 58 | (address _feeRecipient, uint256 _defaultMintFee) = abi.decode(data, (address, uint256)); 59 | setfeeRecipient(_feeRecipient); 60 | setDefaultMintFee(_defaultMintFee); 61 | } 62 | 63 | /// @dev Called by a Core into an Module during the uninstallation of the Module. 64 | function onUninstall(bytes calldata data) external {} 65 | 66 | /*////////////////////////////////////////////////////////////// 67 | Encode install / uninstall data 68 | //////////////////////////////////////////////////////////////*/ 69 | 70 | /// @dev Returns bytes encoded install params, to be sent to `onInstall` function 71 | function encodeBytesOnInstall(address _feeRecipient, uint256 _defaultMintFee) 72 | external 73 | pure 74 | returns (bytes memory) 75 | { 76 | return abi.encode(_feeRecipient, _defaultMintFee); 77 | } 78 | 79 | /// @dev Returns bytes encoded uninstall params, to be sent to `onUninstall` function 80 | function encodeBytesOnUninstall() external pure returns (bytes memory) { 81 | return ""; 82 | } 83 | 84 | /*////////////////////////////////////////////////////////////// 85 | FALLBACK FUNCTIONS 86 | //////////////////////////////////////////////////////////////*/ 87 | 88 | function getfeeRecipient() external view returns (address) { 89 | return _mintFeeManagerStorage().feeRecipient; 90 | } 91 | 92 | function setfeeRecipient(address _feeRecipient) public { 93 | _mintFeeManagerStorage().feeRecipient = _feeRecipient; 94 | 95 | emit feeRecipientUpdated(_feeRecipient); 96 | } 97 | 98 | function getDefaultMintFee() external view returns (uint256) { 99 | return _mintFeeManagerStorage().defaultMintFee; 100 | } 101 | 102 | function setDefaultMintFee(uint256 _defaultMintFee) public { 103 | if (_defaultMintFee > MINT_FEE_MAX_BPS) { 104 | revert MintFeeExceedsMaxBps(); 105 | } 106 | _mintFeeManagerStorage().defaultMintFee = _defaultMintFee; 107 | 108 | emit DefaultMintFeeUpdated(_defaultMintFee); 109 | } 110 | 111 | function getMintFees(address _contract) external view returns (uint256) { 112 | return _mintFeeManagerStorage().mintFees[_contract]; 113 | } 114 | 115 | /** 116 | * @notice updates the mint fee for the specified contract 117 | * @param _contract the address of the token to be updated 118 | * @param _mintFee the new mint fee for the contract 119 | * @dev a mint fee of 0 means they are registered under the default mint fee 120 | */ 121 | function updateMintFee(address _contract, uint256 _mintFee) external { 122 | if (_mintFee > MINT_FEE_MAX_BPS && _mintFee != type(uint256).max) { 123 | revert MintFeeExceedsMaxBps(); 124 | } 125 | _mintFeeManagerStorage().mintFees[_contract] = _mintFee; 126 | 127 | emit MintFeeUpdated(_contract, _mintFee); 128 | } 129 | 130 | /** 131 | * @notice returns the mint fee for the specified contract and the platform fee recipient 132 | * @param _price the price of the token to be minted 133 | * @return the mint fee for the specified contract and the platform fee recipient 134 | * @dev a mint fee of uint256 max means they are subject to zero mint fees 135 | */ 136 | function calculatePlatformFeeAndRecipient(uint256 _price) external view returns (uint256, address) { 137 | uint256 mintFee; 138 | 139 | if (_mintFeeManagerStorage().mintFees[msg.sender] == 0) { 140 | mintFee = (_price * _mintFeeManagerStorage().defaultMintFee) / MINT_FEE_MAX_BPS; 141 | } else if (_mintFeeManagerStorage().mintFees[msg.sender] != type(uint256).max) { 142 | mintFee = (_price * _mintFeeManagerStorage().mintFees[msg.sender]) / MINT_FEE_MAX_BPS; 143 | } 144 | 145 | return (mintFee, _mintFeeManagerStorage().feeRecipient); 146 | } 147 | 148 | function _mintFeeManagerStorage() internal pure returns (MintFeeManagerStorage.Data storage) { 149 | return MintFeeManagerStorage.data(); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/module/token/crosschain/CrossChain.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | abstract contract CrossChain { 5 | 6 | /*////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | error OnCrossChainTransactionSentNotImplemented(); 11 | error OnCrossChainTransactionReceivedNotImplemented(); 12 | 13 | /*////////////////////////////////////////////////////////////// 14 | EXTERNAL FUNCTIONS 15 | //////////////////////////////////////////////////////////////*/ 16 | 17 | /** 18 | * @notice Sends a cross-chain transaction. 19 | * @param _destinationChain The destination chain ID. 20 | * @param _callAddress The address of the contract on the destination chain. 21 | * @param _payload The payload to send to the destination chain. 22 | * @param _extraArgs The extra arguments to pass 23 | * @dev extraArgs may contain items such as token, amount, feeTokenAddress, receipient, gasLimit, etc 24 | */ 25 | function sendCrossChainTransaction( 26 | uint64 _destinationChain, 27 | address _callAddress, 28 | bytes calldata _payload, 29 | bytes calldata _extraArgs 30 | ) external payable virtual; 31 | 32 | /** 33 | * @notice callback function for when a cross-chain transaction is sent. 34 | * @param _destinationChain The destination chain ID. 35 | * @param _callAddress The address of the contract on the destination chain. 36 | * @param _payload The payload sent to the destination chain. 37 | * @param _extraArgs The extra arguments sent to the callAddress on the destination chain. 38 | */ 39 | function onCrossChainTransactionSent( 40 | uint64 _destinationChain, 41 | address _callAddress, 42 | bytes calldata _payload, 43 | bytes calldata _extraArgs 44 | ) internal virtual { 45 | revert OnCrossChainTransactionSentNotImplemented(); 46 | } 47 | 48 | /** 49 | * @notice callback function for when a cross-chain transaction is received. 50 | * @param _sourceChain The source chain ID. 51 | * @param _sourceAddress The address of the contract on the source chain. 52 | * @param _payload The payload sent to the destination chain. 53 | * @param _extraArgs The extra arguments sent to the callAddress on the destination chain. 54 | */ 55 | function onCrossChainTransactionReceived( 56 | uint64 _sourceChain, 57 | address _sourceAddress, 58 | bytes memory _payload, 59 | bytes memory _extraArgs 60 | ) internal virtual { 61 | revert OnCrossChainTransactionReceivedNotImplemented(); 62 | } 63 | 64 | function setRouter(address _router) external virtual; 65 | function getRouter() external view virtual returns (address); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/module/token/crosschain/SuperChainInterop.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Module} from "../../../Module.sol"; 5 | 6 | import {Role} from "../../../Role.sol"; 7 | import {IInstallationCallback} from "../../../interface/IInstallationCallback.sol"; 8 | import {IMintFeeManager} from "../../../interface/IMintFeeManager.sol"; 9 | import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; 10 | import {ERC20} from "@solady/tokens/ERC20.sol"; 11 | import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol"; 12 | 13 | import {BeforeMintCallbackERC20} from "../../../callback/BeforeMintCallbackERC20.sol"; 14 | import {BeforeMintWithSignatureCallbackERC20} from "../../../callback/BeforeMintWithSignatureCallbackERC20.sol"; 15 | 16 | // Inherit from OP repo once implemented 17 | interface ICrossChainERC20 { 18 | 19 | function crosschainMint(address _account, uint256 _amount) external; 20 | function crosschainBurn(address _account, uint256 _amount) external; 21 | 22 | event CrosschainMinted(address indexed _to, uint256 _amount); 23 | event CrosschainBurnt(address indexed _from, uint256 _amount); 24 | 25 | } 26 | 27 | library SuperChainInteropStorage { 28 | 29 | /// @custom:storage-location erc7201:crosschain.superchain.erc20 30 | bytes32 public constant SUPERCHAIN_ERC20_POSITION = 31 | keccak256(abi.encode(uint256(keccak256("crosschain.superchain.erc20")) - 1)) & ~bytes32(uint256(0xff)); 32 | 33 | struct Data { 34 | address superchainBridge; 35 | } 36 | 37 | function data() internal pure returns (Data storage data_) { 38 | bytes32 position = SUPERCHAIN_ERC20_POSITION; 39 | assembly { 40 | data_.slot := position 41 | } 42 | } 43 | 44 | } 45 | 46 | contract SuperChainInterop is ERC20, Module, IInstallationCallback, ICrossChainERC20 { 47 | 48 | /*////////////////////////////////////////////////////////////// 49 | ERRORS 50 | //////////////////////////////////////////////////////////////*/ 51 | 52 | /// @dev Emitted when the minting request signature is unauthorized. 53 | error SuperChainInteropNotSuperChainBridge(); 54 | 55 | /*////////////////////////////////////////////////////////////// 56 | MODULE CONFIG 57 | //////////////////////////////////////////////////////////////*/ 58 | 59 | function getModuleConfig() external pure override returns (ModuleConfig memory config) { 60 | config.fallbackFunctions = new FallbackFunction[](4); 61 | 62 | config.fallbackFunctions[0] = FallbackFunction({selector: this.crosschainMint.selector, permissionBits: 0}); 63 | config.fallbackFunctions[1] = FallbackFunction({selector: this.crosschainBurn.selector, permissionBits: 0}); 64 | config.fallbackFunctions[2] = FallbackFunction({selector: this.getSuperChainBridge.selector, permissionBits: 0}); 65 | config.fallbackFunctions[3] = 66 | FallbackFunction({selector: this.setSuperChainBridge.selector, permissionBits: Role._MANAGER_ROLE}); 67 | 68 | config.requiredInterfaces = new bytes4[](1); 69 | config.requiredInterfaces[0] = 0x36372b07; // ERC20 70 | 71 | config.registerInstallationCallback = true; 72 | } 73 | 74 | /*////////////////////////////////////////////////////////////// 75 | ERC20 FUNCTIONS 76 | //////////////////////////////////////////////////////////////*/ 77 | 78 | /// @dev Returns the name of the token. 79 | function name() public pure override returns (string memory) { 80 | return "SuperChainInterop"; 81 | } 82 | 83 | /// @dev Returns the symbol of the token. 84 | function symbol() public pure override returns (string memory) { 85 | return "SC20"; 86 | } 87 | 88 | /*////////////////////////////////////////////////////////////// 89 | MODIFIERS 90 | //////////////////////////////////////////////////////////////*/ 91 | 92 | modifier onlySuperchainERC20Bridge() { 93 | if (msg.sender != _superchainInteropStorage().superchainBridge) { 94 | revert SuperChainInteropNotSuperChainBridge(); 95 | } 96 | _; 97 | } 98 | 99 | /*////////////////////////////////////////////////////////////// 100 | onInstall / onUninstall 101 | //////////////////////////////////////////////////////////////*/ 102 | 103 | /// @dev Called by a Core into an Module during the installation of the Module. 104 | function onInstall(bytes calldata data) external { 105 | address superchainBridge = abi.decode(data, (address)); 106 | _superchainInteropStorage().superchainBridge = superchainBridge; 107 | } 108 | 109 | /// @dev Called by a Core into an Module during the uninstallation of the Module. 110 | function onUninstall(bytes calldata data) external {} 111 | 112 | /*////////////////////////////////////////////////////////////// 113 | Encode install / uninstall data 114 | //////////////////////////////////////////////////////////////*/ 115 | 116 | /// @dev Returns bytes encoded install params, to be sent to `onInstall` function 117 | function encodeBytesOnInstall(address superchainBridge) external pure returns (bytes memory) { 118 | return abi.encode(superchainBridge); 119 | } 120 | 121 | /// @dev Returns bytes encoded uninstall params, to be sent to `onUninstall` function 122 | function encodeBytesOnUninstall() external pure returns (bytes memory) { 123 | return ""; 124 | } 125 | 126 | /*////////////////////////////////////////////////////////////// 127 | FALLBACK FUNCTIONS 128 | //////////////////////////////////////////////////////////////*/ 129 | 130 | /// @notice performs a crosschain mint 131 | function crosschainMint(address _account, uint256 _amount) external onlySuperchainERC20Bridge { 132 | _mint(_account, _amount); 133 | 134 | emit CrosschainMinted(_account, _amount); 135 | } 136 | 137 | /// @notice performs a crosschain burn 138 | function crosschainBurn(address _account, uint256 _amount) external onlySuperchainERC20Bridge { 139 | _burn(_account, _amount); 140 | 141 | emit CrosschainBurnt(_account, _amount); 142 | } 143 | 144 | /// @notice returns the superchain bridge address 145 | function getSuperChainBridge() external view returns (address) { 146 | return _superchainInteropStorage().superchainBridge; 147 | } 148 | 149 | /// @notice sets the superchain bridge address 150 | function setSuperChainBridge(address _superchainBridge) external { 151 | _superchainInteropStorage().superchainBridge = _superchainBridge; 152 | } 153 | 154 | /*////////////////////////////////////////////////////////////// 155 | INTERNAL FUNCTIONS 156 | //////////////////////////////////////////////////////////////*/ 157 | 158 | function _superchainInteropStorage() internal pure returns (SuperChainInteropStorage.Data storage) { 159 | return SuperChainInteropStorage.data(); 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/module/token/metadata/BatchMetadataERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Role} from "../../../Role.sol"; 5 | 6 | import {UpdateMetadataCallbackERC1155} from "../../../callback/UpdateMetadataCallbackERC1155.sol"; 7 | import {BatchMetadataERC721} from "./BatchMetadataERC721.sol"; 8 | 9 | contract BatchMetadataERC1155 is BatchMetadataERC721, UpdateMetadataCallbackERC1155 { 10 | 11 | /// @notice Returns all implemented callback and module functions. 12 | function getModuleConfig() external pure virtual override returns (ModuleConfig memory config) { 13 | config.callbackFunctions = new CallbackFunction[](2); 14 | config.fallbackFunctions = new FallbackFunction[](6); 15 | 16 | config.callbackFunctions[0] = CallbackFunction(this.onTokenURI.selector); 17 | config.callbackFunctions[1] = CallbackFunction(this.updateMetadataERC1155.selector); 18 | 19 | config.fallbackFunctions[0] = 20 | FallbackFunction({selector: this.uploadMetadata.selector, permissionBits: Role._MINTER_ROLE}); 21 | config.fallbackFunctions[1] = 22 | FallbackFunction({selector: this.setBaseURI.selector, permissionBits: Role._MANAGER_ROLE}); 23 | config.fallbackFunctions[2] = 24 | FallbackFunction({selector: this.getAllMetadataBatches.selector, permissionBits: 0}); 25 | config.fallbackFunctions[3] = FallbackFunction({selector: this.getMetadataBatch.selector, permissionBits: 0}); 26 | config.fallbackFunctions[4] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0}); 27 | config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchIndex.selector, permissionBits: 0}); 28 | 29 | config.requiredInterfaces = new bytes4[](1); 30 | config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155 31 | 32 | config.supportedInterfaces = new bytes4[](1); 33 | config.supportedInterfaces[0] = 0x49064906; // ERC4906. 34 | } 35 | 36 | /// @notice Callback function for updating metadata 37 | function updateMetadataERC1155(address _to, uint256 _startTokenId, uint256 _quantity, string calldata _baseURI) 38 | external 39 | payable 40 | virtual 41 | override 42 | returns (bytes memory) 43 | { 44 | if (_startTokenId < _batchMetadataStorage().nextTokenIdRangeStart) { 45 | revert BatchMetadataMetadataAlreadySet(); 46 | } 47 | _setMetadata(_startTokenId, _quantity, _baseURI); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/module/token/metadata/OpenEditionMetadataERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Role} from "../../../Role.sol"; 5 | import {OpenEditionMetadataERC721} from "./OpenEditionMetadataERC721.sol"; 6 | 7 | contract OpenEditionMetadataERC1155 is OpenEditionMetadataERC721 { 8 | 9 | /// @notice Returns all implemented callback and module functions. 10 | function getModuleConfig() external pure virtual override returns (ModuleConfig memory config) { 11 | config.callbackFunctions = new CallbackFunction[](1); 12 | config.fallbackFunctions = new FallbackFunction[](1); 13 | 14 | config.callbackFunctions[0] = CallbackFunction(this.onTokenURI.selector); 15 | config.fallbackFunctions[0] = 16 | FallbackFunction({selector: this.setSharedMetadata.selector, permissionBits: Role._MINTER_ROLE}); 17 | 18 | config.requiredInterfaces = new bytes4[](1); 19 | config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155 20 | 21 | config.supportedInterfaces = new bytes4[](1); 22 | config.supportedInterfaces[0] = 0x49064906; // ERC4906. 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/module/token/metadata/OpenEditionMetadataERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Module} from "../../../Module.sol"; 5 | import {Role} from "../../../Role.sol"; 6 | 7 | import {Base64} from "@solady/utils/Base64.sol"; 8 | import {LibString} from "@solady/utils/LibString.sol"; 9 | 10 | library OpenEditionMetadataStorage { 11 | 12 | /// @custom:storage-location erc7201:token.metadata.openedition 13 | bytes32 public constant OPEN_EDITION_METADATA_STORAGE_POSITION = 14 | keccak256(abi.encode(uint256(keccak256("token.metadata.openedition")) - 1)) & ~bytes32(uint256(0xff)); 15 | 16 | struct Data { 17 | /// @notice shared token metadata 18 | OpenEditionMetadataERC721.SharedMetadata sharedMetadata; 19 | } 20 | 21 | function data() internal pure returns (Data storage data_) { 22 | bytes32 position = OPEN_EDITION_METADATA_STORAGE_POSITION; 23 | assembly { 24 | data_.slot := position 25 | } 26 | } 27 | 28 | } 29 | 30 | contract OpenEditionMetadataERC721 is Module { 31 | 32 | /*////////////////////////////////////////////////////////////// 33 | STRUCTS 34 | //////////////////////////////////////////////////////////////*/ 35 | 36 | /** 37 | * @notice Structure for metadata shared across all tokens 38 | * 39 | * @param name Shared name of NFT in metadata 40 | * @param description Shared description of NFT in metadata 41 | * @param imageURI Shared URI of image to render for NFTs 42 | * @param animationURI Shared URI of animation to render for NFTs 43 | */ 44 | struct SharedMetadata { 45 | string name; 46 | string description; 47 | string imageURI; 48 | string animationURI; 49 | } 50 | 51 | /*////////////////////////////////////////////////////////////// 52 | EVENTS 53 | //////////////////////////////////////////////////////////////*/ 54 | 55 | /// @dev EIP-4906: Emitted when shared metadata is updated 56 | event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); 57 | 58 | /// @notice Emitted when the metadata URI is queried for non-existent token. 59 | error BatchMetadataNoMetadataForTokenId(); 60 | 61 | /*////////////////////////////////////////////////////////////// 62 | MODULE CONFIG 63 | //////////////////////////////////////////////////////////////*/ 64 | 65 | /// @notice Returns all implemented callback and module functions. 66 | function getModuleConfig() external pure virtual override returns (ModuleConfig memory config) { 67 | config.callbackFunctions = new CallbackFunction[](1); 68 | config.fallbackFunctions = new FallbackFunction[](1); 69 | 70 | config.callbackFunctions[0] = CallbackFunction(this.onTokenURI.selector); 71 | config.fallbackFunctions[0] = 72 | FallbackFunction({selector: this.setSharedMetadata.selector, permissionBits: Role._MINTER_ROLE}); 73 | 74 | config.requiredInterfaces = new bytes4[](1); 75 | config.requiredInterfaces[0] = 0x80ac58cd; // ERC721. 76 | 77 | config.supportedInterfaces = new bytes4[](1); 78 | config.supportedInterfaces[0] = 0x49064906; // ERC4906. 79 | } 80 | 81 | /*////////////////////////////////////////////////////////////// 82 | CALLBACK FUNCTIONS 83 | //////////////////////////////////////////////////////////////*/ 84 | 85 | /// @notice Callback function for ERC721Metadata.tokenURI 86 | function onTokenURI(uint256 _id) external view returns (string memory) { 87 | SharedMetadata memory info = OpenEditionMetadataStorage.data().sharedMetadata; 88 | 89 | if (bytes(info.name).length == 0 && bytes(info.description).length == 0 && bytes(info.imageURI).length == 0) { 90 | revert BatchMetadataNoMetadataForTokenId(); 91 | } 92 | 93 | return _createMetadataEdition({ 94 | name: info.name, 95 | description: info.description, 96 | imageURI: info.imageURI, 97 | animationURI: info.animationURI, 98 | tokenOfEdition: _id 99 | }); 100 | } 101 | 102 | /*////////////////////////////////////////////////////////////// 103 | FALLBACK FUNCTIONS 104 | //////////////////////////////////////////////////////////////*/ 105 | 106 | /// @notice Set shared metadata for NFTs 107 | function setSharedMetadata(SharedMetadata calldata _metadata) external { 108 | OpenEditionMetadataStorage.data().sharedMetadata = _metadata; 109 | 110 | emit BatchMetadataUpdate(0, type(uint256).max); 111 | } 112 | 113 | /*////////////////////////////////////////////////////////////// 114 | INTERNAL FUNCTIONS 115 | //////////////////////////////////////////////////////////////*/ 116 | 117 | /// @dev Generates open edition metadata as base64-json blob 118 | function _createMetadataEdition( 119 | string memory name, 120 | string memory description, 121 | string memory imageURI, 122 | string memory animationURI, 123 | uint256 tokenOfEdition 124 | ) internal pure returns (string memory) { 125 | string memory tokenMediaData = _tokenMediaData(imageURI, animationURI); 126 | bytes memory json = _createMetadataJSON(name, description, tokenMediaData, tokenOfEdition); 127 | return _encodeMetadataJSON(json); 128 | } 129 | 130 | /** 131 | * @param name Name of NFT in metadata 132 | * @param description Description of NFT in metadata 133 | * @param mediaData Data for media to include in json object 134 | * @param tokenOfEdition Token ID for specific token 135 | */ 136 | function _createMetadataJSON( 137 | string memory name, 138 | string memory description, 139 | string memory mediaData, 140 | uint256 tokenOfEdition 141 | ) internal pure returns (bytes memory) { 142 | return abi.encodePacked( 143 | '{"name": "', 144 | name, 145 | " ", 146 | LibString.toString(tokenOfEdition), 147 | '", "', 148 | 'description": "', 149 | description, 150 | '", "', 151 | mediaData, 152 | 'properties": {"number": ', 153 | LibString.toString(tokenOfEdition), 154 | ', "name": "', 155 | name, 156 | '"}}' 157 | ); 158 | } 159 | 160 | /// Encodes the argument json bytes into base64-data uri format 161 | /// @param json Raw json to base64 and turn into a data-uri 162 | function _encodeMetadataJSON(bytes memory json) internal pure returns (string memory) { 163 | return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json))); 164 | } 165 | 166 | /// Generates edition metadata from storage information as base64-json blob 167 | /// Combines the media data and metadata 168 | /// @param imageUrl URL of image to render for edition 169 | /// @param animationUrl URL of animation to render for edition 170 | function _tokenMediaData(string memory imageUrl, string memory animationUrl) 171 | internal 172 | pure 173 | returns (string memory) 174 | { 175 | bool hasImage = bytes(imageUrl).length > 0; 176 | bool hasAnimation = bytes(animationUrl).length > 0; 177 | if (hasImage && hasAnimation) { 178 | return string(abi.encodePacked('image": "', imageUrl, '", "animation_url": "', animationUrl, '", "')); 179 | } 180 | if (hasImage) { 181 | return string(abi.encodePacked('image": "', imageUrl, '", "')); 182 | } 183 | if (hasAnimation) { 184 | return string(abi.encodePacked('animation_url": "', animationUrl, '", "')); 185 | } 186 | 187 | return ""; 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/module/token/tokenId/SequentialTokenIdERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Module} from "../../../Module.sol"; 5 | 6 | import {Role} from "../../../Role.sol"; 7 | 8 | import {UpdateTokenIdCallbackERC1155} from "../../../callback/UpdateTokenIdERC1155.sol"; 9 | import {IInstallationCallback} from "../../../interface/IInstallationCallback.sol"; 10 | 11 | library SequentialTokenIdStorage { 12 | 13 | /// @custom:storage-location erc7201:token.minting.tokenId 14 | bytes32 public constant SEQUENTIAL_TOKEN_ID_STORAGE_POSITION = 15 | keccak256(abi.encode(uint256(keccak256("token.tokenId.erc1155")) - 1)) & ~bytes32(uint256(0xff)); 16 | 17 | struct Data { 18 | uint256 nextTokenId; 19 | } 20 | 21 | function data() internal pure returns (Data storage data_) { 22 | bytes32 position = SEQUENTIAL_TOKEN_ID_STORAGE_POSITION; 23 | assembly { 24 | data_.slot := position 25 | } 26 | } 27 | 28 | } 29 | 30 | contract SequentialTokenIdERC1155 is Module, UpdateTokenIdCallbackERC1155 { 31 | 32 | /*////////////////////////////////////////////////////////////// 33 | ERRORS 34 | //////////////////////////////////////////////////////////////*/ 35 | 36 | /// @dev Emitted when the tokenId is invalid. 37 | error SequentialTokenIdInvalidTokenId(); 38 | 39 | /*////////////////////////////////////////////////////////////// 40 | MODULE CONFIG 41 | //////////////////////////////////////////////////////////////*/ 42 | 43 | /// @notice Returns all implemented callback and fallback functions. 44 | function getModuleConfig() external pure override returns (ModuleConfig memory config) { 45 | config.callbackFunctions = new CallbackFunction[](1); 46 | config.fallbackFunctions = new FallbackFunction[](1); 47 | 48 | config.callbackFunctions[0] = CallbackFunction(this.updateTokenIdERC1155.selector); 49 | 50 | config.fallbackFunctions[0] = FallbackFunction({selector: this.getNextTokenId.selector, permissionBits: 0}); 51 | 52 | config.requiredInterfaces = new bytes4[](1); 53 | config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155 54 | 55 | config.registerInstallationCallback = true; 56 | } 57 | 58 | /*////////////////////////////////////////////////////////////// 59 | CALLBACK FUNCTION 60 | //////////////////////////////////////////////////////////////*/ 61 | 62 | function updateTokenIdERC1155(uint256 _tokenId) external payable override returns (uint256) { 63 | uint256 _nextTokenId = _tokenIdStorage().nextTokenId; 64 | 65 | if (_tokenId == type(uint256).max) { 66 | _tokenIdStorage().nextTokenId = _nextTokenId + 1; 67 | 68 | return _nextTokenId; 69 | } 70 | 71 | if (_tokenId > _nextTokenId) { 72 | revert SequentialTokenIdInvalidTokenId(); 73 | } 74 | 75 | return _tokenId; 76 | } 77 | 78 | /// @dev Called by a Core into an Module during the installation of the Module. 79 | function onInstall(bytes calldata data) external { 80 | uint256 nextTokenId = abi.decode(data, (uint256)); 81 | _tokenIdStorage().nextTokenId = nextTokenId; 82 | } 83 | 84 | /// @dev Called by a Core into an Module during the uninstallation of the Module. 85 | function onUninstall(bytes calldata data) external {} 86 | 87 | /*////////////////////////////////////////////////////////////// 88 | Encode install / uninstall data 89 | //////////////////////////////////////////////////////////////*/ 90 | 91 | /// @dev Returns bytes encoded install params, to be sent to `onInstall` function 92 | function encodeBytesOnInstall(uint256 startTokenId) external pure returns (bytes memory) { 93 | return abi.encode(startTokenId); 94 | } 95 | 96 | /// @dev Returns bytes encoded uninstall params, to be sent to `onUninstall` function 97 | function encodeBytesOnUninstall() external pure returns (bytes memory) { 98 | return ""; 99 | } 100 | 101 | /*////////////////////////////////////////////////////////////// 102 | FALLBACK FUNCTIONS 103 | //////////////////////////////////////////////////////////////*/ 104 | 105 | /// @notice Returns the sale configuration for a token. 106 | function getNextTokenId() external view returns (uint256) { 107 | return _tokenIdStorage().nextTokenId; 108 | } 109 | 110 | function _tokenIdStorage() internal pure returns (SequentialTokenIdStorage.Data storage) { 111 | return SequentialTokenIdStorage.data(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/module/token/transferable/CreatorTokenERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {Module} from "../../../Module.sol"; 5 | import {Role} from "../../../Role.sol"; 6 | 7 | import {BeforeTransferCallbackERC20} from "../../../callback/BeforeTransferCallbackERC20.sol"; 8 | import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; 9 | 10 | import {ICreatorToken} from "@limitbreak/creator-token-standards/interfaces/ICreatorToken.sol"; 11 | 12 | import {ITransferValidator} from "@limitbreak/creator-token-standards/interfaces/ITransferValidator.sol"; 13 | import {ITransferValidatorSetTokenType} from 14 | "@limitbreak/creator-token-standards/interfaces/ITransferValidatorSetTokenType.sol"; 15 | import {TOKEN_TYPE_ERC20} from "@limitbreak/permit-c/Constants.sol"; 16 | 17 | library CreatorTokenStorage { 18 | 19 | /// @custom:storage-location erc7201:creator.token.erc20 20 | bytes32 public constant CREATORTOKEN_STORAGE_POSITION = 21 | keccak256(abi.encode(uint256(keccak256("creator.token.erc20")) - 1)) & ~bytes32(uint256(0xff)); 22 | 23 | struct Data { 24 | // the addresss of the transfer validator 25 | address transferValidator; 26 | } 27 | 28 | function data() internal pure returns (Data storage data_) { 29 | bytes32 position = CREATORTOKEN_STORAGE_POSITION; 30 | assembly { 31 | data_.slot := position 32 | } 33 | } 34 | 35 | } 36 | 37 | contract CreatorTokenERC20 is Module, BeforeTransferCallbackERC20, ICreatorToken { 38 | 39 | /*////////////////////////////////////////////////////////////// 40 | CONSTANTS 41 | //////////////////////////////////////////////////////////////*/ 42 | 43 | bytes32 private constant DEFAULT_ACCESS_CONTROL_ADMIN_ROLE = 0x00; 44 | 45 | /*////////////////////////////////////////////////////////////// 46 | ERRORS 47 | //////////////////////////////////////////////////////////////*/ 48 | 49 | /// @notice Revert with an error if the transfer validator is not valid 50 | error InvalidTransferValidatorContract(); 51 | 52 | /// @notice Revert with an error if the transfer validator is not valid 53 | error NotTransferValidator(); 54 | 55 | /*////////////////////////////////////////////////////////////// 56 | EXTENSION CONFIG 57 | //////////////////////////////////////////////////////////////*/ 58 | 59 | /// @notice Returns all implemented callback and extension functions. 60 | function getModuleConfig() external pure override returns (ModuleConfig memory config) { 61 | config.callbackFunctions = new CallbackFunction[](1); 62 | config.fallbackFunctions = new FallbackFunction[](4); 63 | 64 | config.callbackFunctions[0] = CallbackFunction(this.beforeTransferERC20.selector); 65 | 66 | config.fallbackFunctions[0] = 67 | FallbackFunction({selector: this.getTransferValidator.selector, permissionBits: 0}); 68 | config.fallbackFunctions[1] = 69 | FallbackFunction({selector: this.getTransferValidationFunction.selector, permissionBits: 0}); 70 | config.fallbackFunctions[2] = 71 | FallbackFunction({selector: this.setTransferValidator.selector, permissionBits: Role._MANAGER_ROLE}); 72 | config.fallbackFunctions[3] = FallbackFunction({selector: this.hasRole.selector, permissionBits: 0}); 73 | } 74 | 75 | /*////////////////////////////////////////////////////////////// 76 | CALLBACK FUNCTIONS 77 | //////////////////////////////////////////////////////////////*/ 78 | 79 | /// @notice Callback function for ERC20.transferFrom/safeTransferFrom 80 | function beforeTransferERC20(address from, address to, uint256 amount) 81 | external 82 | virtual 83 | override 84 | returns (bytes memory) 85 | { 86 | address transferValidator = getTransferValidator(); 87 | if (transferValidator != address(0)) { 88 | ITransferValidator(transferValidator).validateTransfer(msg.sender, from, to, 0, amount); 89 | } 90 | } 91 | 92 | /*////////////////////////////////////////////////////////////// 93 | FALLBACK FUNCTIONS 94 | //////////////////////////////////////////////////////////////*/ 95 | 96 | /// @notice Returns the transfer validator contract address for this token contract. 97 | function getTransferValidator() public view returns (address validator) { 98 | validator = _creatorTokenStorage().transferValidator; 99 | } 100 | 101 | /** 102 | * @notice Returns the function selector for the transfer validator's validation function to be called 103 | * @notice for transaction simulation. 104 | */ 105 | function getTransferValidationFunction() external pure returns (bytes4 functionSignature, bool isViewFunction) { 106 | functionSignature = bytes4(keccak256("validateTransfer(address,address,address,uint256,uint256)")); 107 | isViewFunction = true; 108 | } 109 | 110 | function setTransferValidator(address validator) external { 111 | _setTransferValidator(validator); 112 | } 113 | 114 | function hasRole(bytes32 role, address account) external view returns (bool) { 115 | if (msg.sender != _creatorTokenStorage().transferValidator) { 116 | revert NotTransferValidator(); 117 | } 118 | if (role == DEFAULT_ACCESS_CONTROL_ADMIN_ROLE) { 119 | return OwnableRoles(address(this)).hasAllRoles(account, Role._MANAGER_ROLE); 120 | } 121 | return OwnableRoles(address(this)).hasAllRoles(account, uint256(role)); 122 | } 123 | 124 | /*////////////////////////////////////////////////////////////// 125 | INTERNAL FUNCTIONS 126 | //////////////////////////////////////////////////////////////*/ 127 | 128 | function _setTransferValidator(address validator) internal { 129 | bool isValidTransferValidator = validator.code.length > 0; 130 | 131 | if (validator != address(0) && !isValidTransferValidator) { 132 | revert InvalidTransferValidatorContract(); 133 | } 134 | 135 | emit TransferValidatorUpdated(address(getTransferValidator()), validator); 136 | 137 | _creatorTokenStorage().transferValidator = validator; 138 | _registerTokenType(validator); 139 | } 140 | 141 | function _registerTokenType(address validator) internal { 142 | if (validator != address(0)) { 143 | uint256 validatorCodeSize; 144 | assembly { 145 | validatorCodeSize := extcodesize(validator) 146 | } 147 | if (validatorCodeSize > 0) { 148 | try ITransferValidatorSetTokenType(validator).setTokenTypeOfCollection(address(this), _tokenType()) {} 149 | catch {} 150 | } 151 | } 152 | } 153 | 154 | function _tokenType() internal pure virtual returns (uint16 tokenType) { 155 | return uint16(TOKEN_TYPE_ERC20); 156 | } 157 | 158 | function _creatorTokenStorage() internal pure returns (CreatorTokenStorage.Data storage) { 159 | return CreatorTokenStorage.data(); 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/module/token/transferable/TransferableERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Module} from "../../../Module.sol"; 5 | import {Role} from "../../../Role.sol"; 6 | 7 | import {BeforeBatchTransferCallbackERC1155} from "../../../callback/BeforeBatchTransferCallbackERC1155.sol"; 8 | import {BeforeTransferCallbackERC1155} from "../../../callback/BeforeTransferCallbackERC1155.sol"; 9 | 10 | library TransferableStorage { 11 | 12 | /// @custom:storage-location erc7201:token.transferable 13 | bytes32 public constant TRANSFERABLE_STORAGE_POSITION = 14 | keccak256(abi.encode(uint256(keccak256("token.transferable")) - 1)) & ~bytes32(uint256(0xff)); 15 | 16 | struct Data { 17 | // whether transfers are enabled 18 | bool transferEnabled; 19 | // from/to/operator address => bool, whether transfer is enabled 20 | mapping(address => bool) transferEnabledFor; 21 | } 22 | 23 | function data() internal pure returns (Data storage data_) { 24 | bytes32 position = TRANSFERABLE_STORAGE_POSITION; 25 | assembly { 26 | data_.slot := position 27 | } 28 | } 29 | 30 | } 31 | 32 | contract TransferableERC1155 is Module, BeforeTransferCallbackERC1155, BeforeBatchTransferCallbackERC1155 { 33 | 34 | /*////////////////////////////////////////////////////////////// 35 | ERRORS 36 | //////////////////////////////////////////////////////////////*/ 37 | 38 | /// @notice Emitted on attempt to transfer a token when transfers are disabled. 39 | error TransferDisabled(); 40 | 41 | /*////////////////////////////////////////////////////////////// 42 | MODULE CONFIG 43 | //////////////////////////////////////////////////////////////*/ 44 | 45 | /// @notice Returns all implemented callback and module functions. 46 | function getModuleConfig() external pure override returns (ModuleConfig memory config) { 47 | config.callbackFunctions = new CallbackFunction[](2); 48 | config.fallbackFunctions = new FallbackFunction[](4); 49 | 50 | config.callbackFunctions[0] = CallbackFunction(this.beforeTransferERC1155.selector); 51 | config.callbackFunctions[1] = CallbackFunction(this.beforeBatchTransferERC1155.selector); 52 | 53 | config.fallbackFunctions[0] = FallbackFunction({selector: this.isTransferEnabled.selector, permissionBits: 0}); 54 | config.fallbackFunctions[1] = 55 | FallbackFunction({selector: this.isTransferEnabledFor.selector, permissionBits: 0}); 56 | config.fallbackFunctions[2] = 57 | FallbackFunction({selector: this.setTransferable.selector, permissionBits: Role._MANAGER_ROLE}); 58 | config.fallbackFunctions[3] = 59 | FallbackFunction({selector: this.setTransferableFor.selector, permissionBits: Role._MANAGER_ROLE}); 60 | 61 | config.requiredInterfaces = new bytes4[](1); 62 | config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155 63 | } 64 | 65 | /*////////////////////////////////////////////////////////////// 66 | CALLBACK FUNCTIONS 67 | //////////////////////////////////////////////////////////////*/ 68 | 69 | /// @notice Callback function for ERC1155.safeTransferFrom 70 | function beforeTransferERC1155(address from, address to, uint256, uint256) 71 | external 72 | virtual 73 | override 74 | returns (bytes memory) 75 | { 76 | TransferableStorage.Data storage data = _transferableStorage(); 77 | bool isOperatorAllowed = 78 | data.transferEnabledFor[msg.sender] || data.transferEnabledFor[from] || data.transferEnabledFor[to]; 79 | 80 | if (!isOperatorAllowed && !data.transferEnabled) { 81 | revert TransferDisabled(); 82 | } 83 | } 84 | 85 | /// @notice Callback function for ERC1155.safeBatchTransferFrom 86 | function beforeBatchTransferERC1155(address from, address to, uint256[] calldata, uint256[] calldata) 87 | external 88 | virtual 89 | override 90 | returns (bytes memory) 91 | { 92 | TransferableStorage.Data storage data = _transferableStorage(); 93 | bool isOperatorAllowed = 94 | data.transferEnabledFor[msg.sender] || data.transferEnabledFor[from] || data.transferEnabledFor[to]; 95 | 96 | if (!isOperatorAllowed && !data.transferEnabled) { 97 | revert TransferDisabled(); 98 | } 99 | } 100 | 101 | /*////////////////////////////////////////////////////////////// 102 | FALLBACK FUNCTIONS 103 | //////////////////////////////////////////////////////////////*/ 104 | 105 | /// @notice Returns whether transfers is enabled for the token. 106 | function isTransferEnabled() external view returns (bool) { 107 | return _transferableStorage().transferEnabled; 108 | } 109 | 110 | /// @notice Returns whether transfers is enabled for the target for the token. 111 | function isTransferEnabledFor(address target) external view returns (bool) { 112 | return _transferableStorage().transferEnabledFor[target]; 113 | } 114 | 115 | /// @notice Set transferability for a token. 116 | function setTransferable(bool enableTransfer) external { 117 | _transferableStorage().transferEnabled = enableTransfer; 118 | } 119 | 120 | /// @notice Set transferability for an operator for a token. 121 | function setTransferableFor(address target, bool enableTransfer) external { 122 | _transferableStorage().transferEnabledFor[target] = enableTransfer; 123 | } 124 | 125 | /*////////////////////////////////////////////////////////////// 126 | INTERNAL FUNCTIONS 127 | //////////////////////////////////////////////////////////////*/ 128 | 129 | function _transferableStorage() internal pure returns (TransferableStorage.Data storage) { 130 | return TransferableStorage.data(); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/module/token/transferable/TransferableERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Module} from "../../../Module.sol"; 5 | import {Role} from "../../../Role.sol"; 6 | import {BeforeTransferCallbackERC20} from "../../../callback/BeforeTransferCallbackERC20.sol"; 7 | 8 | library TransferableStorage { 9 | 10 | /// @custom:storage-location erc7201:token.transferable 11 | bytes32 public constant TRANSFERABLE_STORAGE_POSITION = 12 | keccak256(abi.encode(uint256(keccak256("token.transferable")) - 1)) & ~bytes32(uint256(0xff)); 13 | 14 | struct Data { 15 | // whether transfers are enabled 16 | bool transferEnabled; 17 | // from/to/operator address => bool, whether transfer is enabled 18 | mapping(address => bool) transferEnabledFor; 19 | } 20 | 21 | function data() internal pure returns (Data storage data_) { 22 | bytes32 position = TRANSFERABLE_STORAGE_POSITION; 23 | assembly { 24 | data_.slot := position 25 | } 26 | } 27 | 28 | } 29 | 30 | contract TransferableERC20 is Module, BeforeTransferCallbackERC20 { 31 | 32 | /*////////////////////////////////////////////////////////////// 33 | ERRORS 34 | //////////////////////////////////////////////////////////////*/ 35 | 36 | /// @notice Emitted on attempt to transfer a token when transfers are disabled. 37 | error TransferDisabled(); 38 | 39 | /*////////////////////////////////////////////////////////////// 40 | MODULE CONFIG 41 | //////////////////////////////////////////////////////////////*/ 42 | 43 | /// @notice Returns all implemented callback and module functions. 44 | function getModuleConfig() external pure override returns (ModuleConfig memory config) { 45 | config.callbackFunctions = new CallbackFunction[](1); 46 | config.fallbackFunctions = new FallbackFunction[](4); 47 | 48 | config.callbackFunctions[0] = CallbackFunction(this.beforeTransferERC20.selector); 49 | 50 | config.fallbackFunctions[0] = FallbackFunction({selector: this.isTransferEnabled.selector, permissionBits: 0}); 51 | config.fallbackFunctions[1] = 52 | FallbackFunction({selector: this.isTransferEnabledFor.selector, permissionBits: 0}); 53 | config.fallbackFunctions[2] = 54 | FallbackFunction({selector: this.setTransferable.selector, permissionBits: Role._MANAGER_ROLE}); 55 | config.fallbackFunctions[3] = 56 | FallbackFunction({selector: this.setTransferableFor.selector, permissionBits: Role._MANAGER_ROLE}); 57 | } 58 | 59 | /*////////////////////////////////////////////////////////////// 60 | CALLBACK FUNCTIONS 61 | //////////////////////////////////////////////////////////////*/ 62 | 63 | /// @notice Callback function for ERC20.transfer 64 | function beforeTransferERC20(address from, address to, uint256) external virtual override returns (bytes memory) { 65 | TransferableStorage.Data storage data = _transferableStorage(); 66 | bool isOperatorAllowed = 67 | data.transferEnabledFor[msg.sender] || data.transferEnabledFor[from] || data.transferEnabledFor[to]; 68 | 69 | if (!isOperatorAllowed && !data.transferEnabled) { 70 | revert TransferDisabled(); 71 | } 72 | } 73 | 74 | /*////////////////////////////////////////////////////////////// 75 | FALLBACK FUNCTIONS 76 | //////////////////////////////////////////////////////////////*/ 77 | 78 | /// @notice Returns whether transfers is enabled for the token. 79 | function isTransferEnabled() external view returns (bool) { 80 | return _transferableStorage().transferEnabled; 81 | } 82 | 83 | /// @notice Returns whether transfers is enabled for the target address for the token. 84 | function isTransferEnabledFor(address target) external view returns (bool) { 85 | return _transferableStorage().transferEnabledFor[target]; 86 | } 87 | 88 | /// @notice Set transferability for a token. 89 | function setTransferable(bool enableTransfer) external { 90 | _transferableStorage().transferEnabled = enableTransfer; 91 | } 92 | 93 | /// @notice Set transferability for an address for a token. 94 | function setTransferableFor(address target, bool enableTransfer) external { 95 | _transferableStorage().transferEnabledFor[target] = enableTransfer; 96 | } 97 | 98 | /*////////////////////////////////////////////////////////////// 99 | INTERNAL FUNCTIONS 100 | //////////////////////////////////////////////////////////////*/ 101 | 102 | function _transferableStorage() internal pure returns (TransferableStorage.Data storage) { 103 | return TransferableStorage.data(); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/module/token/transferable/TransferableERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Module} from "../../../Module.sol"; 5 | import {Role} from "../../../Role.sol"; 6 | import {BeforeTransferCallbackERC721} from "../../../callback/BeforeTransferCallbackERC721.sol"; 7 | 8 | library TransferableStorage { 9 | 10 | /// @custom:storage-location erc7201:token.transferable 11 | bytes32 public constant TRANSFERABLE_STORAGE_POSITION = 12 | keccak256(abi.encode(uint256(keccak256("token.transferable")) - 1)) & ~bytes32(uint256(0xff)); 13 | 14 | struct Data { 15 | // whether transfer is enabled 16 | bool transferEnabled; 17 | // from/to/operator address => bool, whether transfer is enabled 18 | mapping(address => bool) transferEnabledFor; 19 | } 20 | 21 | function data() internal pure returns (Data storage data_) { 22 | bytes32 position = TRANSFERABLE_STORAGE_POSITION; 23 | assembly { 24 | data_.slot := position 25 | } 26 | } 27 | 28 | } 29 | 30 | contract TransferableERC721 is Module, BeforeTransferCallbackERC721 { 31 | 32 | /*////////////////////////////////////////////////////////////// 33 | ERRORS 34 | //////////////////////////////////////////////////////////////*/ 35 | 36 | /// @notice Emitted on attempt to transfer a token when transfers are disabled. 37 | error TransferDisabled(); 38 | 39 | /*////////////////////////////////////////////////////////////// 40 | MODULE CONFIG 41 | //////////////////////////////////////////////////////////////*/ 42 | 43 | /// @notice Returns all implemented callback and module functions. 44 | function getModuleConfig() external pure override returns (ModuleConfig memory config) { 45 | config.callbackFunctions = new CallbackFunction[](1); 46 | config.fallbackFunctions = new FallbackFunction[](4); 47 | 48 | config.callbackFunctions[0] = CallbackFunction(this.beforeTransferERC721.selector); 49 | 50 | config.fallbackFunctions[0] = FallbackFunction({selector: this.isTransferEnabled.selector, permissionBits: 0}); 51 | config.fallbackFunctions[1] = 52 | FallbackFunction({selector: this.isTransferEnabledFor.selector, permissionBits: 0}); 53 | config.fallbackFunctions[2] = 54 | FallbackFunction({selector: this.setTransferable.selector, permissionBits: Role._MANAGER_ROLE}); 55 | config.fallbackFunctions[3] = 56 | FallbackFunction({selector: this.setTransferableFor.selector, permissionBits: Role._MANAGER_ROLE}); 57 | 58 | config.requiredInterfaces = new bytes4[](1); 59 | config.requiredInterfaces[0] = 0x80ac58cd; // ERC721. 60 | } 61 | 62 | /*////////////////////////////////////////////////////////////// 63 | CALLBACK FUNCTIONS 64 | //////////////////////////////////////////////////////////////*/ 65 | 66 | /// @notice Callback function for ERC721.transferFrom/safeTransferFrom 67 | function beforeTransferERC721(address from, address to, uint256) external virtual override returns (bytes memory) { 68 | TransferableStorage.Data storage data = _transferableStorage(); 69 | bool isOperatorAllowed = 70 | data.transferEnabledFor[msg.sender] || data.transferEnabledFor[from] || data.transferEnabledFor[to]; 71 | 72 | if (!isOperatorAllowed && !data.transferEnabled) { 73 | revert TransferDisabled(); 74 | } 75 | } 76 | 77 | /*////////////////////////////////////////////////////////////// 78 | FALLBACK FUNCTIONS 79 | //////////////////////////////////////////////////////////////*/ 80 | 81 | /// @notice Returns whether transfers is enabled for the token. 82 | function isTransferEnabled() external view returns (bool) { 83 | return _transferableStorage().transferEnabled; 84 | } 85 | 86 | /// @notice Returns whether transfers is enabled for the target address for the token. 87 | function isTransferEnabledFor(address target) external view returns (bool) { 88 | return _transferableStorage().transferEnabledFor[target]; 89 | } 90 | 91 | /// @notice Set transferability for a token. 92 | function setTransferable(bool enableTransfer) external { 93 | _transferableStorage().transferEnabled = enableTransfer; 94 | } 95 | 96 | /// @notice Set transferability for an address for a token. 97 | function setTransferableFor(address target, bool enableTransfer) external { 98 | _transferableStorage().transferEnabledFor[target] = enableTransfer; 99 | } 100 | 101 | /*////////////////////////////////////////////////////////////// 102 | INTERNAL FUNCTIONS 103 | //////////////////////////////////////////////////////////////*/ 104 | 105 | function _transferableStorage() internal pure returns (TransferableStorage.Data storage) { 106 | return TransferableStorage.data(); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /test/Interface.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2.0 2 | pragma solidity ^0.8.0; 3 | 4 | // Test utils 5 | import {Test} from "forge-std/Test.sol"; 6 | 7 | import {IERC7572} from "src/interface/IERC7572.sol"; 8 | 9 | contract InterfaceTest is Test { 10 | 11 | event InterfaceId(bytes4 id); 12 | 13 | function testInterface() public { 14 | emit InterfaceId(type(IERC7572).interfaceId); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /test/TWCloneFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "../src/TWCloneFactory.sol"; 5 | import {LibClone} from "@solady/utils/LibClone.sol"; 6 | import "forge-std/Test.sol"; 7 | 8 | contract MockImplementation { 9 | 10 | uint256 public value; 11 | 12 | function initialize(uint256 _value) public { 13 | value = _value; 14 | } 15 | 16 | } 17 | 18 | contract TWCloneFactoryTest is Test { 19 | 20 | TWCloneFactory factory; 21 | MockImplementation implementation; 22 | 23 | uint256 chainId1 = 1; 24 | uint256 chainId2 = 2; 25 | 26 | function setUp() public { 27 | factory = new TWCloneFactory(); 28 | implementation = new MockImplementation(); 29 | } 30 | 31 | // Test case 1: Cross chain deployment on - same address on two chains 32 | function testCrossChainDeploymentOnSameAddress() public { 33 | // Set up the salt with allowCrossChainDeployment = true, encodeDataIntoSalt = false 34 | // salt[0] == hex"01", salt[1] == hex"00" 35 | bytes32 salt = bytes32(abi.encodePacked(hex"01", hex"00", bytes30(0))); 36 | bytes memory data = ""; 37 | 38 | // Save the current state 39 | uint256 snapshotId = vm.snapshot(); 40 | 41 | // Set chain ID to chainId1 and deploy 42 | vm.chainId(chainId1); 43 | address proxy1 = factory.deployProxyByImplementation(address(implementation), data, salt); 44 | 45 | // Revert to the snapshot to reset the state 46 | vm.revertTo(snapshotId); 47 | 48 | // Set chain ID to chainId2 and deploy 49 | vm.chainId(chainId2); 50 | address proxy2 = factory.deployProxyByImplementation(address(implementation), data, salt); 51 | 52 | // Check that the proxies are the same 53 | assertEq(proxy1, proxy2, "Proxies should be the same on different chains when cross chain deployment is on"); 54 | } 55 | 56 | // Test case 2: Cross chain deployment off - different address on two chains 57 | function testCrossChainDeploymentOffDifferentAddress() public { 58 | // Set up the salt with allowCrossChainDeployment = false, encodeDataIntoSalt = false 59 | // salt[0] == hex"00", salt[1] == hex"00" 60 | bytes32 salt = bytes32(abi.encodePacked(hex"00", hex"00", bytes30(0))); 61 | bytes memory data = ""; 62 | 63 | // Save the current state 64 | uint256 snapshotId = vm.snapshot(); 65 | 66 | // Set chain ID to chainId1 and deploy 67 | vm.chainId(chainId1); 68 | address proxy1 = factory.deployProxyByImplementation(address(implementation), data, salt); 69 | 70 | // Revert to the snapshot to reset the state 71 | vm.revertTo(snapshotId); 72 | 73 | // Set chain ID to chainId2 and deploy 74 | vm.chainId(chainId2); 75 | address proxy2 = factory.deployProxyByImplementation(address(implementation), data, salt); 76 | 77 | // Check that the proxies are different 78 | assertNotEq( 79 | proxy1, proxy2, "Proxies should be different on different chains when cross chain deployment is off" 80 | ); 81 | } 82 | 83 | // Test case 3: Not encodeDataIntoSalt - same address with different init data 84 | function testNotEncodeDataIntoSaltSameAddress() public { 85 | // Set up the salt with allowCrossChainDeployment = true, encodeDataIntoSalt = false 86 | // salt[0] == hex"01", salt[1] == hex"00" 87 | bytes32 salt = bytes32(abi.encodePacked(hex"01", hex"00", bytes30(0))); 88 | 89 | // Save the current state 90 | uint256 snapshotId = vm.snapshot(); 91 | 92 | // Deploy with data1 93 | vm.chainId(chainId1); 94 | bytes memory data1 = abi.encodeWithSignature("initialize(uint256)", 42); 95 | address proxy1 = factory.deployProxyByImplementation(address(implementation), data1, salt); 96 | 97 | // Revert to the snapshot to reset the state 98 | vm.revertTo(snapshotId); 99 | 100 | // Deploy with data2 101 | vm.chainId(chainId2); 102 | bytes memory data2 = abi.encodeWithSignature("initialize(uint256)", 100); 103 | address proxy2 = factory.deployProxyByImplementation(address(implementation), data2, salt); 104 | 105 | // Check that the proxies are the same 106 | assertEq(proxy1, proxy2, "Proxies should be the same when encodeDataIntoSalt is off"); 107 | } 108 | 109 | // Test case 4: Encode Data into salt - different address with different init data 110 | function testEncodeDataIntoSaltDifferentAddress() public { 111 | // Set up the salt with allowCrossChainDeployment = true, encodeDataIntoSalt = true 112 | // salt[0] == hex"01", salt[1] == hex"01" 113 | bytes32 salt = bytes32(abi.encodePacked(hex"01", hex"01", bytes30(0))); 114 | 115 | // Deploy with data1 116 | bytes memory data1 = abi.encodeWithSignature("initialize(uint256)", 42); 117 | address proxy1 = factory.deployProxyByImplementation(address(implementation), data1, salt); 118 | 119 | // Deploy with data2 120 | bytes memory data2 = abi.encodeWithSignature("initialize(uint256)", 100); 121 | address proxy2 = factory.deployProxyByImplementation(address(implementation), data2, salt); 122 | 123 | // Check that the proxies are different 124 | assertNotEq(proxy1, proxy2, "Proxies should be different when encodeDataIntoSalt is on"); 125 | 126 | // Verify that both initializations took effect independently 127 | MockImplementation proxyImpl1 = MockImplementation(proxy1); 128 | uint256 value1 = proxyImpl1.value(); 129 | assertEq(value1, 42, "Value should be initialized to 42"); 130 | 131 | MockImplementation proxyImpl2 = MockImplementation(proxy2); 132 | uint256 value2 = proxyImpl2.value(); 133 | assertEq(value2, 100, "Value should be initialized to 100"); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /test/mock/MockMintFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | contract MockMintFeeManager { 5 | 6 | function calculatePlatformFeeAndRecipient(uint256 _price) external view returns (uint256, address) { 7 | return ((_price * 100) / 10_000, address(0x3)); 8 | } 9 | 10 | function getPlatformFeeReceipient() external view returns (address) { 11 | return address(0x3); 12 | } 13 | 14 | function getPrimarySaleAndPlatformFeeAmount(uint256 _price) external view returns (uint256, uint256) { 15 | uint256 platformFeeAmount = (_price * 100) / 10_000; 16 | uint256 primarySaleAmount = _price - platformFeeAmount; 17 | return (primarySaleAmount, platformFeeAmount); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /test/module/MintFeeManager.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {Test} from "forge-std/Test.sol"; 7 | import {LibClone} from "solady/utils/LibClone.sol"; 8 | import {Role} from "src/Role.sol"; 9 | 10 | import {MintFeeManagerCore} from "src/core/MintFeeManagerCore.sol"; 11 | import {MintFeeManagerModule} from "src/module/MintFeeManagerModule.sol"; 12 | 13 | contract MintFeeManagerTest is Test { 14 | 15 | address public mintFeeManagerCore; 16 | MintFeeManagerModule public mintFeeManagerModule; 17 | 18 | address public owner = address(0x123); 19 | address public feeRecipient = address(0x456); 20 | uint256 public defaultMintFee = 100; 21 | address public unauthorizedActor = address(0x3); 22 | 23 | address public contract1 = address(0x1); 24 | address public contract2 = address(0x2); 25 | 26 | event MintFeeUpdated(address indexed contractAddress, uint256 mintFee); 27 | event DefaultMintFeeUpdated(uint256 mintFee); 28 | 29 | function setUp() public { 30 | // Deploy the contract 31 | address[] memory modules; 32 | bytes[] memory moduleData; 33 | mintFeeManagerCore = address(new MintFeeManagerCore(owner, modules, moduleData)); 34 | mintFeeManagerModule = new MintFeeManagerModule(); 35 | 36 | bytes memory initializeData = mintFeeManagerModule.encodeBytesOnInstall(feeRecipient, defaultMintFee); 37 | vm.prank(owner); 38 | MintFeeManagerCore(payable(mintFeeManagerCore)).installModule(address(mintFeeManagerModule), initializeData); 39 | 40 | vm.prank(owner); 41 | MintFeeManagerCore(payable(mintFeeManagerCore)).grantRoles(owner, Role._MANAGER_ROLE); 42 | } 43 | 44 | /*/////////////////////////////////////////////////////////////// 45 | Unit tests: `setfeeRecipient` 46 | //////////////////////////////////////////////////////////////*/ 47 | 48 | function test_state_setfeeRecipient() public { 49 | vm.prank(owner); 50 | MintFeeManagerModule(mintFeeManagerCore).setfeeRecipient(address(0x456)); 51 | 52 | assertEq(MintFeeManagerModule(mintFeeManagerCore).getfeeRecipient(), address(0x456)); 53 | } 54 | 55 | function test_revert_setfeeRecipient_unauthorizedCaller() public { 56 | vm.prank(unauthorizedActor); 57 | vm.expectRevert(abi.encodeWithSelector(0x82b42900)); // Unauthorized() 58 | MintFeeManagerModule(mintFeeManagerCore).setfeeRecipient(address(0x456)); 59 | } 60 | 61 | /*/////////////////////////////////////////////////////////////// 62 | Unit tests: `updateMintFee` 63 | //////////////////////////////////////////////////////////////*/ 64 | 65 | function test_state_updateMintFee() public { 66 | vm.startPrank(owner); 67 | vm.expectEmit(true, true, true, true); 68 | emit MintFeeUpdated(contract1, 500); 69 | MintFeeManagerModule(mintFeeManagerCore).updateMintFee(contract1, 500); 70 | 71 | vm.expectEmit(true, true, true, true); 72 | emit MintFeeUpdated(contract2, type(uint256).max); 73 | MintFeeManagerModule(mintFeeManagerCore).updateMintFee(contract2, type(uint256).max); 74 | vm.stopPrank(); 75 | 76 | assertEq(MintFeeManagerModule(mintFeeManagerCore).getMintFees(contract1), 500); 77 | assertEq(MintFeeManagerModule(mintFeeManagerCore).getMintFees(contract2), type(uint256).max); 78 | } 79 | 80 | function test_revert_updateMintFee_unauthorizedCaller() public { 81 | vm.prank(unauthorizedActor); 82 | vm.expectRevert(abi.encodeWithSelector(0x82b42900)); // Unauthorized() 83 | MintFeeManagerModule(mintFeeManagerCore).updateMintFee(contract1, 100); 84 | } 85 | 86 | function test_revert_updateMintFee_invalidMintFee() public { 87 | vm.prank(owner); 88 | vm.expectRevert(abi.encodeWithSelector(MintFeeManagerModule.MintFeeExceedsMaxBps.selector)); 89 | MintFeeManagerModule(mintFeeManagerCore).updateMintFee(contract1, 10_001); 90 | } 91 | 92 | /*/////////////////////////////////////////////////////////////// 93 | Unit tests: `setDefaultMintFee` 94 | //////////////////////////////////////////////////////////////*/ 95 | 96 | function test_state_setDefaultMintFee() public { 97 | vm.prank(owner); 98 | vm.expectEmit(true, true, true, true); 99 | emit DefaultMintFeeUpdated(500); 100 | MintFeeManagerModule(mintFeeManagerCore).setDefaultMintFee(500); 101 | 102 | assertEq(MintFeeManagerModule(mintFeeManagerCore).getDefaultMintFee(), 500); 103 | } 104 | 105 | function test_revert_setDefaultMintFee_unauthorizedCaller() public { 106 | vm.prank(unauthorizedActor); 107 | vm.expectRevert(abi.encodeWithSelector(0x82b42900)); // Unauthorized() 108 | MintFeeManagerModule(mintFeeManagerCore).setDefaultMintFee(100); 109 | } 110 | 111 | function test_revert_setDefaultMintFee_invalidMintFee() public { 112 | vm.prank(owner); 113 | vm.expectRevert(abi.encodeWithSelector(MintFeeManagerModule.MintFeeExceedsMaxBps.selector)); 114 | MintFeeManagerModule(mintFeeManagerCore).setDefaultMintFee(10_001); 115 | } 116 | 117 | /*/////////////////////////////////////////////////////////////// 118 | Unit tests: `calculatePlatformFeeAndRecipient` 119 | //////////////////////////////////////////////////////////////*/ 120 | 121 | function test_state_calculatePlatformFeeAndRecipient() public { 122 | vm.prank(owner); 123 | MintFeeManagerModule(mintFeeManagerCore).updateMintFee(contract1, 500); 124 | 125 | vm.prank(contract1); 126 | (uint256 mintFee, address _feeRecipient) = 127 | MintFeeManagerModule(mintFeeManagerCore).calculatePlatformFeeAndRecipient(100); 128 | 129 | assertEq(mintFee, 5); 130 | assertEq(_feeRecipient, feeRecipient); 131 | } 132 | 133 | function test_state_calculatePlatformFeeAndRecipient_defaultMintFee() public { 134 | vm.prank(contract1); 135 | (uint256 mintFee, address _feeRecipient) = 136 | MintFeeManagerModule(mintFeeManagerCore).calculatePlatformFeeAndRecipient(100); 137 | 138 | assertEq(mintFee, 1); 139 | assertEq(_feeRecipient, feeRecipient); 140 | } 141 | 142 | function test_state_calculatePlatformFeeAndRecipient_zeroMintFee() public { 143 | vm.prank(owner); 144 | MintFeeManagerModule(mintFeeManagerCore).updateMintFee(contract1, type(uint256).max); 145 | 146 | vm.prank(contract1); 147 | (uint256 mintFee, address _feeRecipient) = 148 | MintFeeManagerModule(mintFeeManagerCore).calculatePlatformFeeAndRecipient(100); 149 | 150 | assertEq(mintFee, 0); 151 | assertEq(_feeRecipient, feeRecipient); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /test/module/SplitWallet.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {Test, Vm} from "forge-std/Test.sol"; 7 | 8 | // Target contracts 9 | import {Core} from "src/Core.sol"; 10 | import {Module} from "src/Module.sol"; 11 | import {Role} from "src/Role.sol"; 12 | import {SplitFeesCore} from "src/core/SplitFeesCore.sol"; 13 | 14 | import {SplitWallet} from "src/core/SplitWallet.sol"; 15 | import {SplitFeesModule} from "src/module/SplitFeesModule.sol"; 16 | 17 | import {ERC20} from "@solady/tokens/ERC20.sol"; 18 | import {ISplitWallet} from "src/interface/ISplitWallet.sol"; 19 | import {Split} from "src/libraries/Split.sol"; 20 | 21 | contract MockCurrency is ERC20 { 22 | 23 | function mintTo(address _recipient, uint256 _amount) public { 24 | _mint(_recipient, _amount); 25 | } 26 | 27 | /// @dev Returns the name of the token. 28 | function name() public view virtual override returns (string memory) { 29 | return "MockCurrency"; 30 | } 31 | 32 | /// @dev Returns the symbol of the token. 33 | function symbol() public view virtual override returns (string memory) { 34 | return "MOCK"; 35 | } 36 | 37 | } 38 | 39 | contract SplitFeesModuleTest is Test { 40 | 41 | SplitFeesCore public splitFeesCore; 42 | SplitFeesModule public splitFeesModule; 43 | 44 | MockCurrency public token; 45 | 46 | address public splitWallet; 47 | 48 | address public owner = address(0x1); 49 | address public recipient1 = address(0x4); 50 | address public recipient2 = address(0x5); 51 | 52 | // Constants 53 | address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; 54 | 55 | function setUp() public { 56 | // Instantiate SplitFeesCore and install SplitFeesModule 57 | address[] memory modules; 58 | bytes[] memory moduleData; 59 | splitFeesCore = new SplitFeesCore(owner, modules, moduleData); 60 | splitFeesModule = new SplitFeesModule(); 61 | 62 | vm.prank(owner); 63 | splitFeesCore.installModule(address(splitFeesModule), ""); 64 | 65 | // Create a split using the module 66 | (address[] memory recipients, uint256[] memory allocations) = getRecipientsAndAllocations(); 67 | 68 | vm.recordLogs(); 69 | vm.prank(owner); 70 | SplitFeesModule(address(splitFeesCore)).createSplit(recipients, allocations, owner); 71 | 72 | // Retrieve the splitWallet address from the event logs 73 | Vm.Log[] memory entries = vm.getRecordedLogs(); 74 | 75 | bytes32 SplitCreatedTopic = keccak256("SplitCreated(address,address[],uint256[],address)"); 76 | 77 | for (uint256 i = 0; i < entries.length; i++) { 78 | Vm.Log memory log = entries[i]; 79 | if (log.topics[0] == SplitCreatedTopic) { 80 | // This is the SplitCreated event 81 | splitWallet = address(uint160(uint256(log.topics[1]))); 82 | break; 83 | } 84 | } 85 | 86 | token = new MockCurrency(); 87 | } 88 | 89 | function getRecipientsAndAllocations() public view returns (address[] memory, uint256[] memory) { 90 | address[] memory recipients = new address[](2); 91 | uint256[] memory allocations = new uint256[](2); 92 | recipients[0] = recipient1; 93 | recipients[1] = recipient2; 94 | allocations[0] = 50; 95 | allocations[1] = 50; 96 | 97 | return (recipients, allocations); 98 | } 99 | 100 | /*////////////////////////////////////////////////////////////// 101 | Unit tests: distribute and withdraw 102 | //////////////////////////////////////////////////////////////*/ 103 | 104 | function test_distribute_and_withdraw_ETH() public { 105 | // Deposit ETH to splitWallet 106 | vm.deal(splitWallet, 1 ether); 107 | 108 | // Check initial balances 109 | uint256 initialBalance1 = recipient1.balance; 110 | uint256 initialBalance2 = recipient2.balance; 111 | 112 | // Distribute ETH 113 | splitFeesCore.distribute(splitWallet, NATIVE_TOKEN_ADDRESS); 114 | 115 | // After distribute, balances should be updated in the ERC6909 contract 116 | uint256 tokenId = uint256(uint160(NATIVE_TOKEN_ADDRESS)); 117 | 118 | // Check that recipients have the correct balances 119 | uint256 balance1 = splitFeesCore.balanceOf(recipient1, tokenId); 120 | uint256 balance2 = splitFeesCore.balanceOf(recipient2, tokenId); 121 | 122 | assertEq(balance1, 0.5 ether, "Recipient1 should have 0.5 ether balance"); 123 | assertEq(balance2, 0.5 ether, "Recipient2 should have 0.5 ether balance"); 124 | 125 | // Now recipients can withdraw their balances 126 | vm.prank(recipient1); 127 | splitFeesCore.withdraw(recipient1, NATIVE_TOKEN_ADDRESS); 128 | 129 | vm.prank(recipient2); 130 | splitFeesCore.withdraw(recipient2, NATIVE_TOKEN_ADDRESS); 131 | 132 | // Check that recipients received ETH 133 | assertEq(recipient1.balance, initialBalance1 + 0.5 ether, "Recipient1 should have received 0.5 ether"); 134 | assertEq(recipient2.balance, initialBalance2 + 0.5 ether, "Recipient2 should have received 0.5 ether"); 135 | 136 | // Check that their balances in the ERC6909 contract are zero 137 | balance1 = splitFeesCore.balanceOf(recipient1, tokenId); 138 | balance2 = splitFeesCore.balanceOf(recipient2, tokenId); 139 | 140 | assertEq(balance1, 0, "Recipient1 should have zero balance after withdrawal"); 141 | assertEq(balance2, 0, "Recipient2 should have zero balance after withdrawal"); 142 | } 143 | 144 | function test_distribute_and_withdraw_ERC20() public { 145 | // Mint tokens to splitWallet 146 | uint256 amount = 1000 ether; 147 | token.mintTo(splitWallet, amount); 148 | 149 | // Check initial balances 150 | uint256 initialBalance1 = token.balanceOf(recipient1); 151 | uint256 initialBalance2 = token.balanceOf(recipient2); 152 | 153 | // Distribute ERC20 tokens 154 | splitFeesCore.distribute(splitWallet, address(token)); 155 | 156 | // After distribute, balances should be updated in the ERC6909 contract 157 | uint256 tokenId = uint256(uint160(address(token))); 158 | 159 | // Check that recipients have the correct balances 160 | uint256 balance1 = splitFeesCore.balanceOf(recipient1, tokenId); 161 | uint256 balance2 = splitFeesCore.balanceOf(recipient2, tokenId); 162 | 163 | assertEq(balance1, amount / 2, "Recipient1 should have half of the tokens"); 164 | assertEq(balance2, amount / 2, "Recipient2 should have half of the tokens"); 165 | 166 | // Now recipients can withdraw their balances 167 | vm.prank(recipient1); 168 | splitFeesCore.withdraw(recipient1, address(token)); 169 | 170 | vm.prank(recipient2); 171 | splitFeesCore.withdraw(recipient2, address(token)); 172 | 173 | // Check that recipients received tokens 174 | assertEq(token.balanceOf(recipient1), initialBalance1 + amount / 2, "Recipient1 should have received tokens"); 175 | assertEq(token.balanceOf(recipient2), initialBalance2 + amount / 2, "Recipient2 should have received tokens"); 176 | 177 | // Check that their balances in the ERC6909 contract are zero 178 | balance1 = splitFeesCore.balanceOf(recipient1, tokenId); 179 | balance2 = splitFeesCore.balanceOf(recipient2, tokenId); 180 | 181 | assertEq(balance1, 0, "Recipient1 should have zero balance after withdrawal"); 182 | assertEq(balance2, 0, "Recipient2 should have zero balance after withdrawal"); 183 | } 184 | 185 | function test_revert_notSplitFees() public { 186 | vm.expectRevert(abi.encodeWithSelector(SplitWallet.OnlySplitFees.selector)); 187 | SplitWallet(splitWallet).transferETH(10 ether); 188 | 189 | vm.expectRevert(abi.encodeWithSelector(SplitWallet.OnlySplitFees.selector)); 190 | SplitWallet(splitWallet).transferERC20(address(token), 10 ether); 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /test/module/crosschain/SuperChainInterop.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; 9 | import {ERC20} from "@solady/tokens/ERC20.sol"; 10 | 11 | // Target contract 12 | 13 | import {Module} from "src/Module.sol"; 14 | 15 | import {Role} from "src/Role.sol"; 16 | import {ERC20Core} from "src/core/token/ERC20Core.sol"; 17 | 18 | import {ICore} from "src/interface/ICore.sol"; 19 | import {IModuleConfig} from "src/interface/IModuleConfig.sol"; 20 | import {SuperChainInterop} from "src/module/token/crosschain/SuperChainInterop.sol"; 21 | 22 | contract Core is ERC20Core { 23 | 24 | constructor( 25 | string memory name, 26 | string memory symbol, 27 | string memory contractURI, 28 | address owner, 29 | address[] memory modules, 30 | bytes[] memory moduleInstallData 31 | ) payable ERC20Core(name, symbol, contractURI, owner, modules, moduleInstallData) {} 32 | 33 | // disable mint callbacks for these tests 34 | function _beforeMint(address to, uint256 amount, bytes calldata data) internal override {} 35 | 36 | } 37 | 38 | contract MintableERC20Test is Test { 39 | 40 | Core public core; 41 | 42 | SuperChainInterop public superchainInterop; 43 | 44 | uint256 ownerPrivateKey = 1; 45 | address public owner; 46 | address public superchainBridge = address(0x123); 47 | address public actor1 = address(0x111); 48 | address public unpermissionedActor = address(0x222); 49 | 50 | event CrosschainMinted(address indexed _to, uint256 _amount); 51 | event CrosschainBurnt(address indexed _from, uint256 _amount); 52 | 53 | function setUp() public { 54 | owner = vm.addr(ownerPrivateKey); 55 | 56 | address[] memory modules; 57 | bytes[] memory moduleData; 58 | 59 | core = new Core("test", "TEST", "", owner, modules, moduleData); 60 | superchainInterop = new SuperChainInterop(); 61 | 62 | // install module 63 | bytes memory encodedInstallParams = superchainInterop.encodeBytesOnInstall(superchainBridge); 64 | vm.prank(owner); 65 | core.installModule(address(superchainInterop), encodedInstallParams); 66 | } 67 | 68 | /*////////////////////////////////////////////////////////////// 69 | Tests: get / set SuperChainBridge 70 | //////////////////////////////////////////////////////////////*/ 71 | 72 | function test_state_setSuperChainBridge() public { 73 | vm.prank(owner); 74 | SuperChainInterop(address(core)).setSuperChainBridge(address(0x123)); 75 | 76 | assertEq(SuperChainInterop(address(core)).getSuperChainBridge(), address(0x123)); 77 | } 78 | 79 | function test_revert_setSuperChainBridge_unauthorizedCaller() public { 80 | vm.prank(unpermissionedActor); 81 | vm.expectRevert(abi.encodeWithSelector(0x82b42900)); // Unauthorized() 82 | SuperChainInterop(address(core)).setSuperChainBridge(address(0x123)); 83 | } 84 | 85 | function test_getSuperChainBridge_state() public { 86 | assertEq(SuperChainInterop(address(core)).getSuperChainBridge(), superchainBridge); 87 | } 88 | 89 | /*////////////////////////////////////////////////////////////// 90 | Tests: CrossChainMint 91 | //////////////////////////////////////////////////////////////*/ 92 | 93 | function test_crosschainMint_state() public { 94 | uint256 balanceBefore = core.balanceOf(actor1); 95 | assertEq(balanceBefore, 0); 96 | 97 | vm.prank(superchainBridge); 98 | vm.expectEmit(true, true, true, true); 99 | emit CrosschainMinted(actor1, 10 ether); 100 | SuperChainInterop(address(core)).crosschainMint(actor1, 10 ether); 101 | 102 | assertEq(core.balanceOf(actor1), 10 ether); 103 | } 104 | 105 | function test_crosschainMint_revert_unauthorizedCaller() public { 106 | vm.prank(unpermissionedActor); 107 | vm.expectRevert(abi.encodeWithSelector(SuperChainInterop.SuperChainInteropNotSuperChainBridge.selector)); 108 | SuperChainInterop(address(core)).crosschainMint(actor1, 10 ether); 109 | } 110 | 111 | /*////////////////////////////////////////////////////////////// 112 | Tests: CrossChainBurn 113 | //////////////////////////////////////////////////////////////*/ 114 | 115 | function test_crosschainBurn_state() public { 116 | core.mint(actor1, 10 ether, ""); 117 | 118 | uint256 balanceBefore = core.balanceOf(actor1); 119 | assertEq(balanceBefore, 10 ether); 120 | 121 | vm.prank(superchainBridge); 122 | vm.expectEmit(true, true, true, true); 123 | emit CrosschainBurnt(actor1, 10 ether); 124 | SuperChainInterop(address(core)).crosschainBurn(actor1, 10 ether); 125 | 126 | assertEq(core.balanceOf(actor1), 0); 127 | } 128 | 129 | function test_crosschainBurn_revert_unauthorizedCaller() public { 130 | vm.prank(unpermissionedActor); 131 | vm.expectRevert(abi.encodeWithSelector(SuperChainInterop.SuperChainInteropNotSuperChainBridge.selector)); 132 | SuperChainInterop(address(core)).crosschainBurn(actor1, 10 ether); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /test/module/metadata/BatchMetadataERC1155.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // Target contract 9 | 10 | import {Core} from "src/Core.sol"; 11 | import {Module} from "src/Module.sol"; 12 | import {Role} from "src/Role.sol"; 13 | import {ERC1155Core} from "src/core/token/ERC1155Core.sol"; 14 | 15 | import {ICore} from "src/interface/ICore.sol"; 16 | import {IModuleConfig} from "src/interface/IModuleConfig.sol"; 17 | 18 | import {BatchMetadataERC1155} from "src/module/token/metadata/BatchMetadataERC1155.sol"; 19 | import {BatchMetadataERC721} from "src/module/token/metadata/BatchMetadataERC721.sol"; 20 | import {MintableERC1155} from "src/module/token/minting/MintableERC1155.sol"; 21 | 22 | contract BatchMetadataExt is BatchMetadataERC1155 {} 23 | 24 | contract BatchMetadataERC1155Test is Test { 25 | 26 | ERC1155Core public core; 27 | 28 | BatchMetadataExt public moduleImplementation; 29 | MintableERC1155 public mintableModule; 30 | 31 | address public owner = address(0x1); 32 | address public permissionedActor = address(0x2); 33 | address public unpermissionedActor = address(0x3); 34 | 35 | function setUp() public { 36 | address[] memory modules; 37 | bytes[] memory moduleData; 38 | 39 | core = new ERC1155Core("test", "TEST", "", owner, modules, moduleData); 40 | moduleImplementation = new BatchMetadataExt(); 41 | mintableModule = new MintableERC1155(address(0x0)); 42 | 43 | // install module 44 | vm.prank(owner); 45 | core.installModule(address(moduleImplementation), ""); 46 | 47 | bytes memory encodedInstallParams = abi.encode(owner); 48 | vm.prank(owner); 49 | core.installModule(address(mintableModule), encodedInstallParams); 50 | 51 | vm.prank(owner); 52 | core.grantRoles(owner, Role._MINTER_ROLE); 53 | } 54 | 55 | /*/////////////////////////////////////////////////////////////// 56 | Unit tests: `getBatchIndex` 57 | //////////////////////////////////////////////////////////////*/ 58 | 59 | function test_state_getBatchIndex() public { 60 | vm.prank(owner); 61 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 62 | uint256 batchIndex = BatchMetadataExt(address(core)).getBatchIndex(50); 63 | 64 | assertEq(batchIndex, 0); 65 | } 66 | 67 | function test_revert_getBatchIndex_noMetadata() public { 68 | vm.prank(owner); 69 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 70 | 71 | vm.expectRevert(BatchMetadataERC721.BatchMetadataNoMetadataForTokenId.selector); 72 | BatchMetadataExt(address(core)).getBatchIndex(101); 73 | } 74 | 75 | /*/////////////////////////////////////////////////////////////// 76 | Unit tests: `getMetadataBatch` 77 | //////////////////////////////////////////////////////////////*/ 78 | 79 | function test_state_getMetadataBatch() public { 80 | vm.startPrank(owner); 81 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 82 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base2/"); 83 | vm.stopPrank(); 84 | 85 | BatchMetadataERC1155.MetadataBatch memory batch = BatchMetadataExt(address(core)).getMetadataBatch(0); 86 | BatchMetadataERC1155.MetadataBatch memory batch2 = BatchMetadataExt(address(core)).getMetadataBatch(1); 87 | 88 | assertEq(batch.startTokenIdInclusive, 0); 89 | assertEq(batch.endTokenIdInclusive, 99); 90 | assertEq(batch.baseURI, "ipfs://base/"); 91 | assertEq(batch2.startTokenIdInclusive, 100); 92 | assertEq(batch2.endTokenIdInclusive, 199); 93 | assertEq(batch2.baseURI, "ipfs://base2/"); 94 | } 95 | 96 | function test_revert_getMetadataBatch_invalidIndex() public { 97 | vm.prank(owner); 98 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 99 | 100 | vm.expectRevert(); 101 | vm.prank(owner); 102 | BatchMetadataExt(address(core)).getMetadataBatch(1); 103 | } 104 | 105 | /*/////////////////////////////////////////////////////////////// 106 | Unit tests: `setBaseURI` 107 | //////////////////////////////////////////////////////////////*/ 108 | 109 | function test_state_setBaseURI() public { 110 | vm.prank(owner); 111 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 112 | 113 | // get metadata batches 114 | BatchMetadataExt.MetadataBatch[] memory batches = BatchMetadataExt(address(core)).getAllMetadataBatches(); 115 | 116 | assertEq(batches.length, 1); 117 | assertEq(batches[0].baseURI, "ipfs://base/"); 118 | 119 | vm.prank(owner); 120 | BatchMetadataExt(address(core)).setBaseURI(0, "ipfs://base2/"); 121 | 122 | // get metadata batches 123 | BatchMetadataExt.MetadataBatch[] memory batches2 = BatchMetadataExt(address(core)).getAllMetadataBatches(); 124 | 125 | assertEq(batches2.length, 1); 126 | assertEq(batches2[0].baseURI, "ipfs://base2/"); 127 | } 128 | 129 | /*/////////////////////////////////////////////////////////////// 130 | Unit tests: `uploadMetadata` 131 | //////////////////////////////////////////////////////////////*/ 132 | 133 | function test_state_uploadMetadata() public { 134 | vm.prank(owner); 135 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 136 | 137 | // read state from core 138 | assertEq(core.uri(1), "ipfs://base/1"); 139 | assertEq(core.uri(99), "ipfs://base/99"); 140 | 141 | // upload another batch 142 | vm.prank(owner); 143 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base2/"); 144 | 145 | // read state from core 146 | assertEq(core.uri(1), "ipfs://base/1"); 147 | assertEq(core.uri(99), "ipfs://base/99"); 148 | assertEq(core.uri(100), "ipfs://base2/0"); 149 | assertEq(core.uri(199), "ipfs://base2/99"); 150 | } 151 | 152 | function test_revert_uploadMetadata() public { 153 | vm.expectRevert(0x82b42900); // `Unauthorized()` 154 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 155 | } 156 | 157 | function test_getAllMetadataBatches() public { 158 | vm.prank(owner); 159 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 160 | 161 | // get metadata batches 162 | BatchMetadataExt.MetadataBatch[] memory batches = BatchMetadataExt(address(core)).getAllMetadataBatches(); 163 | 164 | assertEq(batches.length, 1); 165 | assertEq(batches[0].startTokenIdInclusive, 0); 166 | assertEq(batches[0].endTokenIdInclusive, 99); 167 | assertEq(batches[0].baseURI, "ipfs://base/"); 168 | } 169 | 170 | function test_getNextTokenIdToMint() public { 171 | vm.prank(owner); 172 | BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); 173 | 174 | // get metadata batches 175 | uint256 nextTokenIdToMint = BatchMetadataExt(address(core)).nextTokenIdToMint(); 176 | 177 | assertEq(nextTokenIdToMint, 100); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /test/module/metadata/OpenEditionMetadataERC1155.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // Target contract 9 | 10 | import {Core} from "src/Core.sol"; 11 | import {Module} from "src/Module.sol"; 12 | import {ERC1155Core} from "src/core/token/ERC1155Core.sol"; 13 | 14 | import {ICore} from "src/interface/ICore.sol"; 15 | import {IModuleConfig} from "src/interface/IModuleConfig.sol"; 16 | import {OpenEditionMetadataERC1155} from "src/module/token/metadata/OpenEditionMetadataERC1155.sol"; 17 | import { 18 | OpenEditionMetadataERC721, 19 | OpenEditionMetadataStorage 20 | } from "src/module/token/metadata/OpenEditionMetadataERC721.sol"; 21 | 22 | contract OpenEditionMetadataExt is OpenEditionMetadataERC1155 { 23 | 24 | function sharedMetadata() external view returns (SharedMetadata memory) { 25 | return OpenEditionMetadataStorage.data().sharedMetadata; 26 | } 27 | 28 | function createMetadataEdition( 29 | string memory name, 30 | string memory description, 31 | string memory imageURI, 32 | string memory animationURI, 33 | uint256 tokenOfEdition 34 | ) external pure returns (string memory) { 35 | return _createMetadataEdition(name, description, imageURI, animationURI, tokenOfEdition); 36 | } 37 | 38 | } 39 | 40 | contract OpenEditionMetadataERC1155Test is Test { 41 | 42 | ERC1155Core public core; 43 | 44 | OpenEditionMetadataExt public moduleImplementation; 45 | OpenEditionMetadataExt public installedModule; 46 | 47 | address public owner = address(0x1); 48 | address public permissionedActor = address(0x2); 49 | address public unpermissionedActor = address(0x3); 50 | 51 | function setUp() public { 52 | address[] memory modules; 53 | bytes[] memory moduleData; 54 | 55 | core = new ERC1155Core("test", "TEST", "", owner, modules, moduleData); 56 | moduleImplementation = new OpenEditionMetadataExt(); 57 | 58 | // install module 59 | vm.prank(owner); 60 | core.installModule(address(moduleImplementation), ""); 61 | 62 | ICore.InstalledModule[] memory installedModules = core.getInstalledModules(); 63 | installedModule = OpenEditionMetadataExt(installedModules[0].implementation); 64 | } 65 | 66 | /*/////////////////////////////////////////////////////////////// 67 | Unit tests: `setSharedMetadata` 68 | //////////////////////////////////////////////////////////////*/ 69 | 70 | function test_state_setSharedMetadata() public { 71 | OpenEditionMetadataERC1155.SharedMetadata memory sharedMetadata = OpenEditionMetadataERC721.SharedMetadata({ 72 | name: "Test", 73 | description: "Test", 74 | imageURI: "https://test.com", 75 | animationURI: "https://test.com" 76 | }); 77 | 78 | vm.prank(owner); 79 | OpenEditionMetadataExt(address(core)).setSharedMetadata(sharedMetadata); 80 | 81 | // read state from core 82 | assertEq( 83 | core.uri(1), 84 | installedModule.createMetadataEdition( 85 | sharedMetadata.name, sharedMetadata.description, sharedMetadata.imageURI, sharedMetadata.animationURI, 1 86 | ) 87 | ); 88 | } 89 | 90 | function test_revert_setSharedMetadata() public { 91 | OpenEditionMetadataERC1155.SharedMetadata memory sharedMetadata; 92 | 93 | vm.expectRevert(0x82b42900); // `Unauthorized()` 94 | OpenEditionMetadataExt(address(core)).setSharedMetadata(sharedMetadata); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /test/module/metadata/OpenEditionMetadataERC721.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // Target contract 9 | 10 | import {Core} from "src/Core.sol"; 11 | import {Module} from "src/Module.sol"; 12 | import {ERC721Core} from "src/core/token/ERC721Core.sol"; 13 | 14 | import {ICore} from "src/interface/ICore.sol"; 15 | import {IModuleConfig} from "src/interface/IModuleConfig.sol"; 16 | import { 17 | OpenEditionMetadataERC721, 18 | OpenEditionMetadataStorage 19 | } from "src/module/token/metadata/OpenEditionMetadataERC721.sol"; 20 | 21 | contract OpenEditionMetadataExt is OpenEditionMetadataERC721 { 22 | 23 | function sharedMetadata() external view returns (SharedMetadata memory) { 24 | return OpenEditionMetadataStorage.data().sharedMetadata; 25 | } 26 | 27 | function createMetadataEdition( 28 | string memory name, 29 | string memory description, 30 | string memory imageURI, 31 | string memory animationURI, 32 | uint256 tokenOfEdition 33 | ) external pure returns (string memory) { 34 | return _createMetadataEdition(name, description, imageURI, animationURI, tokenOfEdition); 35 | } 36 | 37 | } 38 | 39 | contract OpenEditionMetadataERC721Test is Test { 40 | 41 | ERC721Core public core; 42 | 43 | OpenEditionMetadataExt public moduleImplementation; 44 | OpenEditionMetadataExt public installedModule; 45 | 46 | address public owner = address(0x1); 47 | address public permissionedActor = address(0x2); 48 | address public unpermissionedActor = address(0x3); 49 | 50 | function setUp() public { 51 | address[] memory modules; 52 | bytes[] memory moduleData; 53 | 54 | core = new ERC721Core("test", "TEST", "", owner, modules, moduleData); 55 | moduleImplementation = new OpenEditionMetadataExt(); 56 | 57 | // install module 58 | vm.prank(owner); 59 | core.installModule(address(moduleImplementation), ""); 60 | 61 | ICore.InstalledModule[] memory installedModules = core.getInstalledModules(); 62 | installedModule = OpenEditionMetadataExt(installedModules[0].implementation); 63 | } 64 | 65 | /*/////////////////////////////////////////////////////////////// 66 | Unit tests: `setSharedMetadata` 67 | //////////////////////////////////////////////////////////////*/ 68 | 69 | function test_state_setSharedMetadata() public { 70 | OpenEditionMetadataERC721.SharedMetadata memory sharedMetadata = OpenEditionMetadataERC721.SharedMetadata({ 71 | name: "Test", 72 | description: "Test", 73 | imageURI: "https://test.com", 74 | animationURI: "https://test.com" 75 | }); 76 | 77 | vm.prank(owner); 78 | OpenEditionMetadataExt(address(core)).setSharedMetadata(sharedMetadata); 79 | 80 | // read state from core 81 | assertEq( 82 | core.tokenURI(1), 83 | installedModule.createMetadataEdition( 84 | sharedMetadata.name, sharedMetadata.description, sharedMetadata.imageURI, sharedMetadata.animationURI, 1 85 | ) 86 | ); 87 | } 88 | 89 | function test_revert_setSharedMetadata() public { 90 | OpenEditionMetadataERC721.SharedMetadata memory sharedMetadata; 91 | 92 | vm.expectRevert(0x82b42900); // `Unauthorized()` 93 | OpenEditionMetadataExt(address(core)).setSharedMetadata(sharedMetadata); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /test/module/tokenId/SequentialTokenIdERC1155.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; 7 | import {ERC20} from "@solady/tokens/ERC20.sol"; 8 | import {Test} from "forge-std/Test.sol"; 9 | 10 | // Target contract 11 | 12 | import {Core} from "src/Core.sol"; 13 | import {Module} from "src/Module.sol"; 14 | 15 | import {Role} from "src/Role.sol"; 16 | import {ERC1155Core} from "src/core/token/ERC1155Core.sol"; 17 | 18 | import {ICore} from "src/interface/ICore.sol"; 19 | import {IModuleConfig} from "src/interface/IModuleConfig.sol"; 20 | 21 | import {BatchMetadataERC1155} from "src/module/token/metadata/BatchMetadataERC1155.sol"; 22 | import {BatchMetadataERC721} from "src/module/token/metadata/BatchMetadataERC721.sol"; 23 | import {MintableERC1155} from "src/module/token/minting/MintableERC1155.sol"; 24 | import {SequentialTokenIdERC1155} from "src/module/token/tokenId/SequentialTokenIdERC1155.sol"; 25 | 26 | contract MockCurrency is ERC20 { 27 | 28 | function mintTo(address _recipient, uint256 _amount) public { 29 | _mint(_recipient, _amount); 30 | } 31 | 32 | /// @dev Returns the name of the token. 33 | function name() public view virtual override returns (string memory) { 34 | return "MockCurrency"; 35 | } 36 | 37 | /// @dev Returns the symbol of the token. 38 | function symbol() public view virtual override returns (string memory) { 39 | return "MOCK"; 40 | } 41 | 42 | } 43 | 44 | contract MintableERC1155Test is Test { 45 | 46 | ERC1155Core public core; 47 | 48 | MintableERC1155 public mintableModule; 49 | BatchMetadataERC1155 public batchMetadataModule; 50 | SequentialTokenIdERC1155 public sequentialTokenIdModule; 51 | 52 | uint256 ownerPrivateKey = 1; 53 | address public owner; 54 | 55 | address tokenRecipient = address(0x123); 56 | 57 | MintableERC1155.MintSignatureParamsERC1155 public mintRequest; 58 | 59 | // Constants 60 | address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; 61 | 62 | function setUp() public { 63 | owner = vm.addr(ownerPrivateKey); 64 | 65 | address[] memory modules; 66 | bytes[] memory moduleData; 67 | 68 | core = new ERC1155Core("test", "TEST", "", owner, modules, moduleData); 69 | mintableModule = new MintableERC1155(address(0x0)); 70 | batchMetadataModule = new BatchMetadataERC1155(); 71 | sequentialTokenIdModule = new SequentialTokenIdERC1155(); 72 | 73 | // install module 74 | bytes memory encodedInstallParams = abi.encode(owner); 75 | vm.prank(owner); 76 | core.installModule(address(mintableModule), encodedInstallParams); 77 | 78 | vm.prank(owner); 79 | core.installModule(address(batchMetadataModule), ""); 80 | 81 | bytes memory encodedTokenIdInstallParams = sequentialTokenIdModule.encodeBytesOnInstall(0); 82 | vm.prank(owner); 83 | core.installModule(address(sequentialTokenIdModule), encodedTokenIdInstallParams); 84 | 85 | // Give permissioned actor minter role 86 | vm.prank(owner); 87 | core.grantRoles(owner, Role._MINTER_ROLE); 88 | } 89 | 90 | /*////////////////////////////////////////////////////////////// 91 | Tests: onInstall 92 | //////////////////////////////////////////////////////////////*/ 93 | 94 | function test_onInstall() public { 95 | vm.startPrank(owner); 96 | core.uninstallModule(address(sequentialTokenIdModule), ""); 97 | core.installModule(address(sequentialTokenIdModule), sequentialTokenIdModule.encodeBytesOnInstall(5)); 98 | vm.stopPrank(); 99 | 100 | assertEq(SequentialTokenIdERC1155(address(core)).getNextTokenId(), 5); 101 | } 102 | 103 | /*////////////////////////////////////////////////////////////// 104 | Tests: beforeMintERC1155 105 | //////////////////////////////////////////////////////////////*/ 106 | 107 | function test_state_updateTokenId() public { 108 | assertEq(SequentialTokenIdERC1155(address(core)).getNextTokenId(), 0); 109 | 110 | // increments the tokenId 111 | vm.prank(owner); 112 | core.mint(owner, type(uint256).max, 10, "", ""); 113 | 114 | assertEq(SequentialTokenIdERC1155(address(core)).getNextTokenId(), 1); 115 | 116 | // does not increment the tokenId 117 | vm.prank(owner); 118 | core.mint(owner, 1, 10, "", ""); 119 | 120 | assertEq(SequentialTokenIdERC1155(address(core)).getNextTokenId(), 1); 121 | } 122 | 123 | function test_revert_updateTokenId() public { 124 | vm.expectRevert(SequentialTokenIdERC1155.SequentialTokenIdInvalidTokenId.selector); 125 | vm.prank(owner); 126 | core.mint(owner, 2, 1, "", ""); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /test/module/transferable/TransferableERC20.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // Target contract 9 | 10 | import {Module} from "src/Module.sol"; 11 | import {ERC20Core} from "src/core/token/ERC20Core.sol"; 12 | 13 | import {ICore} from "src/interface/ICore.sol"; 14 | import {IModuleConfig} from "src/interface/IModuleConfig.sol"; 15 | import {TransferableERC20} from "src/module/token/transferable/TransferableERC20.sol"; 16 | 17 | contract TransferableExt is TransferableERC20 {} 18 | 19 | contract Core is ERC20Core { 20 | 21 | constructor( 22 | string memory name, 23 | string memory symbol, 24 | string memory contractURI, 25 | address owner, 26 | address[] memory modules, 27 | bytes[] memory moduleInstallData 28 | ) payable ERC20Core(name, symbol, contractURI, owner, modules, moduleInstallData) {} 29 | 30 | // disable mint and approve callbacks for these tests 31 | function _beforeMint(address to, uint256 amount, bytes calldata data) internal override {} 32 | function _beforeApprove(address from, address to, uint256 amount) internal override {} 33 | 34 | } 35 | 36 | contract TransferableERC20Test is Test { 37 | 38 | Core public core; 39 | 40 | TransferableExt public moduleImplementation; 41 | TransferableExt public installedModule; 42 | 43 | address public owner = address(0x1); 44 | address public actorOne = address(0x2); 45 | address public actorTwo = address(0x3); 46 | address public actorThree = address(0x4); 47 | 48 | function setUp() public { 49 | address[] memory modules; 50 | bytes[] memory moduleData; 51 | 52 | core = new Core("test", "TEST", "", owner, modules, moduleData); 53 | moduleImplementation = new TransferableExt(); 54 | 55 | // install module 56 | vm.prank(owner); 57 | core.installModule(address(moduleImplementation), ""); 58 | 59 | ICore.InstalledModule[] memory installedModules = core.getInstalledModules(); 60 | installedModule = TransferableExt(installedModules[0].implementation); 61 | 62 | // mint tokens 63 | core.mint(actorOne, 10 ether, ""); 64 | core.mint(actorTwo, 10 ether, ""); 65 | core.mint(actorThree, 10 ether, ""); 66 | } 67 | 68 | /*/////////////////////////////////////////////////////////////// 69 | Unit tests: `setTransferable` 70 | //////////////////////////////////////////////////////////////*/ 71 | 72 | function test_state_setTransferable() public { 73 | // transfers enabled globally 74 | vm.prank(owner); 75 | TransferableExt(address(core)).setTransferable(true); 76 | 77 | // transfer tokens 78 | vm.prank(actorOne); 79 | core.transfer(actorTwo, 2 ether); 80 | 81 | // read state from core 82 | assertEq(core.balanceOf(actorOne), 8 ether); 83 | assertEq(core.balanceOf(actorTwo), 12 ether); 84 | assertEq(TransferableExt(address(core)).isTransferEnabled(), true); 85 | 86 | // transfers disabled globally 87 | vm.prank(owner); 88 | TransferableExt(address(core)).setTransferable(false); 89 | 90 | assertEq(TransferableExt(address(core)).isTransferEnabled(), false); 91 | 92 | // should revert on transfer tokens 93 | vm.prank(actorTwo); 94 | vm.expectRevert(TransferableERC20.TransferDisabled.selector); 95 | core.transfer(actorOne, 1); 96 | } 97 | 98 | function test_revert_setTransferable() public { 99 | vm.expectRevert(0x82b42900); // `Unauthorized()` 100 | TransferableExt(address(core)).setTransferable(true); 101 | } 102 | 103 | /*/////////////////////////////////////////////////////////////// 104 | Unit tests: `setTransferableFor` 105 | //////////////////////////////////////////////////////////////*/ 106 | 107 | function test_state_setTransferableFor_from() public { 108 | // transfers disabled globally 109 | vm.startPrank(owner); 110 | TransferableExt(address(core)).setTransferable(false); 111 | TransferableExt(address(core)).setTransferableFor(actorOne, true); 112 | vm.stopPrank(); 113 | 114 | // transfer tokens 115 | vm.prank(actorOne); 116 | core.transfer(actorTwo, 2 ether); 117 | 118 | // read state from core 119 | assertEq(core.balanceOf(actorOne), 8 ether); 120 | assertEq(core.balanceOf(actorTwo), 12 ether); 121 | assertEq(TransferableExt(address(core)).isTransferEnabled(), false); 122 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorOne), true); 123 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorTwo), false); 124 | 125 | // should revert when transfer not enabled for 126 | vm.prank(actorTwo); 127 | vm.expectRevert(TransferableERC20.TransferDisabled.selector); 128 | core.transfer(actorThree, 1); 129 | } 130 | 131 | function test_state_setTransferableFor_to() public { 132 | // transfers disabled globally 133 | vm.startPrank(owner); 134 | TransferableExt(address(core)).setTransferable(false); 135 | TransferableExt(address(core)).setTransferableFor(actorTwo, true); 136 | vm.stopPrank(); 137 | 138 | // transfer tokens 139 | vm.prank(actorOne); 140 | core.transfer(actorTwo, 2 ether); 141 | 142 | // read state from core 143 | assertEq(core.balanceOf(actorOne), 8 ether); 144 | assertEq(core.balanceOf(actorTwo), 12 ether); 145 | assertEq(TransferableExt(address(core)).isTransferEnabled(), false); 146 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorOne), false); 147 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorTwo), true); 148 | 149 | // revert when transfers not enabled for 150 | vm.prank(actorOne); 151 | vm.expectRevert(TransferableERC20.TransferDisabled.selector); 152 | core.transfer(actorThree, 1); 153 | } 154 | 155 | function test_state_setTransferableFor_operator() public { 156 | // transfers disabled globally 157 | vm.startPrank(owner); 158 | TransferableExt(address(core)).setTransferable(false); 159 | TransferableExt(address(core)).setTransferableFor(actorOne, true); 160 | vm.stopPrank(); 161 | 162 | // approve tokens to operator actorOne 163 | vm.prank(actorTwo); 164 | core.approve(actorOne, type(uint256).max); 165 | 166 | // transfer tokens 167 | vm.prank(actorOne); 168 | core.transferFrom(actorTwo, actorThree, 2 ether); 169 | 170 | // read state from core 171 | assertEq(core.balanceOf(actorTwo), 8 ether); 172 | assertEq(core.balanceOf(actorThree), 12 ether); 173 | assertEq(TransferableExt(address(core)).isTransferEnabled(), false); 174 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorOne), true); 175 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorTwo), false); 176 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorThree), false); 177 | 178 | // revert when transfers not enabled for 179 | vm.prank(actorTwo); 180 | vm.expectRevert(TransferableERC20.TransferDisabled.selector); 181 | core.transferFrom(actorTwo, actorThree, 0); 182 | } 183 | 184 | function test_revert_setTransferableFor() public { 185 | vm.expectRevert(0x82b42900); // `Unauthorized()` 186 | TransferableExt(address(core)).setTransferableFor(actorOne, true); 187 | } 188 | 189 | function test_burner_should_not_need_to_approve_to_themselves() public { 190 | vm.startPrank(actorOne); 191 | core.burn(actorOne, 1, ""); 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /test/module/transferable/TransferableERC721.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "lib/forge-std/src/console.sol"; 5 | 6 | import {Test} from "forge-std/Test.sol"; 7 | 8 | // Target contract 9 | 10 | import {Module} from "src/Module.sol"; 11 | import {ERC721Core} from "src/core/token/ERC721Core.sol"; 12 | 13 | import {ICore} from "src/interface/ICore.sol"; 14 | import {IModuleConfig} from "src/interface/IModuleConfig.sol"; 15 | import {TransferableERC721} from "src/module/token/transferable/TransferableERC721.sol"; 16 | 17 | contract TransferableExt is TransferableERC721 {} 18 | 19 | contract Core is ERC721Core { 20 | 21 | constructor( 22 | string memory name, 23 | string memory symbol, 24 | string memory contractURI, 25 | address owner, 26 | address[] memory modules, 27 | bytes[] memory moduleInstallData 28 | ) ERC721Core(name, symbol, contractURI, owner, modules, moduleInstallData) {} 29 | 30 | // disable mint and approve callbacks for these tests 31 | function _beforeMint(address to, uint256 startTokenId, uint256 quantity, bytes calldata data) internal override {} 32 | function _beforeApproveForAll(address from, address to, bool approved) internal override {} 33 | 34 | } 35 | 36 | contract TransferableERC721Test is Test { 37 | 38 | Core public core; 39 | 40 | TransferableExt public moduleImplementation; 41 | TransferableExt public installedModule; 42 | 43 | address public owner = address(0x1); 44 | address public actorOne = address(0x2); 45 | address public actorTwo = address(0x3); 46 | address public actorThree = address(0x4); 47 | 48 | function setUp() public { 49 | address[] memory modules; 50 | bytes[] memory moduleData; 51 | 52 | core = new Core("test", "TEST", "", owner, modules, moduleData); 53 | moduleImplementation = new TransferableExt(); 54 | 55 | // install module 56 | vm.prank(owner); 57 | core.installModule(address(moduleImplementation), ""); 58 | 59 | ICore.InstalledModule[] memory installedModules = core.getInstalledModules(); 60 | installedModule = TransferableExt(installedModules[0].implementation); 61 | 62 | // mint tokens 63 | core.mint(actorOne, 1, string(""), ""); // tokenId 0 64 | core.mint(actorTwo, 1, string(""), ""); // tokenId 1 65 | core.mint(actorThree, 1, string(""), ""); // tokenId 2 66 | } 67 | 68 | /*/////////////////////////////////////////////////////////////// 69 | Unit tests: `setTransferable` 70 | //////////////////////////////////////////////////////////////*/ 71 | 72 | function test_state_setTransferable() public { 73 | // transfers enabled globally 74 | vm.prank(owner); 75 | TransferableExt(address(core)).setTransferable(true); 76 | 77 | // transfer tokens 78 | vm.prank(actorOne); 79 | core.transferFrom(actorOne, actorTwo, 0); 80 | 81 | // read state from core 82 | assertEq(core.ownerOf(0), actorTwo); 83 | assertEq(core.ownerOf(1), actorTwo); 84 | assertEq(core.balanceOf(actorOne), 0); 85 | assertEq(core.balanceOf(actorTwo), 2); 86 | assertEq(TransferableExt(address(core)).isTransferEnabled(), true); 87 | 88 | // transfers disabled globally 89 | vm.prank(owner); 90 | TransferableExt(address(core)).setTransferable(false); 91 | 92 | assertEq(TransferableExt(address(core)).isTransferEnabled(), false); 93 | 94 | // should revert on transfer tokens 95 | vm.prank(actorTwo); 96 | vm.expectRevert(TransferableERC721.TransferDisabled.selector); 97 | core.transferFrom(actorTwo, actorOne, 0); 98 | } 99 | 100 | function test_revert_setTransferable() public { 101 | vm.expectRevert(0x82b42900); // `Unauthorized()` 102 | TransferableExt(address(core)).setTransferable(true); 103 | } 104 | 105 | /*/////////////////////////////////////////////////////////////// 106 | Unit tests: `setTransferableFor` 107 | //////////////////////////////////////////////////////////////*/ 108 | 109 | function test_state_setTransferableFor_from() public { 110 | // transfers disabled globally 111 | vm.startPrank(owner); 112 | TransferableExt(address(core)).setTransferable(false); 113 | TransferableExt(address(core)).setTransferableFor(actorOne, true); 114 | vm.stopPrank(); 115 | 116 | // transfer tokens 117 | vm.prank(actorOne); 118 | core.transferFrom(actorOne, actorTwo, 0); 119 | 120 | // read state from core 121 | assertEq(core.ownerOf(0), actorTwo); 122 | assertEq(core.ownerOf(1), actorTwo); 123 | assertEq(core.balanceOf(actorOne), 0); 124 | assertEq(core.balanceOf(actorTwo), 2); 125 | assertEq(TransferableExt(address(core)).isTransferEnabled(), false); 126 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorOne), true); 127 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorTwo), false); 128 | 129 | // revert when transfers not enabled for 130 | vm.prank(actorTwo); 131 | vm.expectRevert(TransferableERC721.TransferDisabled.selector); 132 | core.transferFrom(actorTwo, actorThree, 0); 133 | } 134 | 135 | function test_state_setTransferableFor_to() public { 136 | // transfers disabled globally 137 | vm.startPrank(owner); 138 | TransferableExt(address(core)).setTransferable(false); 139 | TransferableExt(address(core)).setTransferableFor(actorTwo, true); 140 | vm.stopPrank(); 141 | 142 | // transfer tokens 143 | vm.prank(actorOne); 144 | core.transferFrom(actorOne, actorTwo, 0); 145 | 146 | // read state from core 147 | assertEq(core.ownerOf(0), actorTwo); 148 | assertEq(core.ownerOf(1), actorTwo); 149 | assertEq(core.balanceOf(actorOne), 0); 150 | assertEq(core.balanceOf(actorTwo), 2); 151 | assertEq(TransferableExt(address(core)).isTransferEnabled(), false); 152 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorOne), false); 153 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorTwo), true); 154 | 155 | // revert when transfers not enabled for 156 | vm.prank(actorOne); 157 | vm.expectRevert(TransferableERC721.TransferDisabled.selector); 158 | core.transferFrom(actorOne, actorThree, 0); 159 | } 160 | 161 | function test_state_setTransferableFor_operator() public { 162 | // transfers disabled globally 163 | vm.startPrank(owner); 164 | TransferableExt(address(core)).setTransferable(false); 165 | TransferableExt(address(core)).setTransferableFor(actorOne, true); 166 | vm.stopPrank(); 167 | 168 | // approve tokens to operator actorOne 169 | vm.prank(actorTwo); 170 | core.setApprovalForAll(actorOne, true); 171 | 172 | // transfer tokens 173 | vm.prank(actorOne); 174 | core.transferFrom(actorTwo, actorThree, 1); 175 | 176 | // read state from core 177 | assertEq(core.ownerOf(1), actorThree); 178 | assertEq(core.ownerOf(2), actorThree); 179 | assertEq(core.balanceOf(actorTwo), 0); 180 | assertEq(core.balanceOf(actorThree), 2); 181 | assertEq(TransferableExt(address(core)).isTransferEnabled(), false); 182 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorOne), true); 183 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorTwo), false); 184 | assertEq(TransferableExt(address(core)).isTransferEnabledFor(actorThree), false); 185 | 186 | // revert when transfers not enabled for 187 | vm.prank(actorTwo); 188 | vm.expectRevert(TransferableERC721.TransferDisabled.selector); 189 | core.transferFrom(actorTwo, actorThree, 0); 190 | } 191 | 192 | function test_revert_setTransferableFor() public { 193 | vm.expectRevert(0x82b42900); // `Unauthorized()` 194 | TransferableExt(address(core)).setTransferableFor(actorOne, true); 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /test/utils/ExtensionProxyFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "@solady/utils/ERC1967Factory.sol"; 5 | 6 | contract ModuleProxyFactory is ERC1967Factory {} 7 | -------------------------------------------------------------------------------- /test/utils/TestPlus.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | // credit Solady (https://github.com/Vectorized/solady) 5 | contract TestPlus { 6 | 7 | event LogString(string name, string value); 8 | event LogBytes(string name, bytes value); 9 | event LogUint(string name, uint256 value); 10 | event LogInt(string name, int256 value); 11 | 12 | /// @dev `address(bytes20(uint160(uint256(keccak256("hevm cheat code")))))`. 13 | address private constant _VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; 14 | 15 | /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). 16 | /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. 17 | /// e.g. `testSomething(uint256) public`. 18 | function _random() internal returns (uint256 r) { 19 | /// @solidity memory-safe-assembly 20 | assembly { 21 | // This is the keccak256 of a very long string I randomly mashed on my keyboard. 22 | let sSlot := 0xd715531fe383f818c5f158c342925dcf01b954d24678ada4d07c36af0f20e1ee 23 | let sValue := sload(sSlot) 24 | 25 | mstore(0x20, sValue) 26 | r := keccak256(0x20, 0x40) 27 | 28 | // If the storage is uninitialized, initialize it to the keccak256 of the calldata. 29 | if iszero(sValue) { 30 | sValue := sSlot 31 | let m := mload(0x40) 32 | calldatacopy(m, 0, calldatasize()) 33 | r := keccak256(m, calldatasize()) 34 | } 35 | sstore(sSlot, add(r, 1)) 36 | 37 | // Do some biased sampling for more robust tests. 38 | // prettier-ignore 39 | for {} 1 {} { 40 | let d := byte(0, r) 41 | // With a 1/256 chance, randomly set `r` to any of 0,1,2. 42 | if iszero(d) { 43 | r := and(r, 3) 44 | break 45 | } 46 | // With a 1/2 chance, set `r` to near a random power of 2. 47 | if iszero(and(2, d)) { 48 | // Set `t` either `not(0)` or `xor(sValue, r)`. 49 | let t := xor(not(0), mul(iszero(and(4, d)), not(xor(sValue, r)))) 50 | // Set `r` to `t` shifted left or right by a random multiple of 8. 51 | switch and(8, d) 52 | case 0 { 53 | if iszero(and(16, d)) { t := 1 } 54 | r := add(shl(shl(3, and(byte(3, r), 0x1f)), t), sub(and(r, 7), 3)) 55 | } 56 | default { 57 | if iszero(and(16, d)) { t := shl(255, 1) } 58 | r := add(shr(shl(3, and(byte(3, r), 0x1f)), t), sub(and(r, 7), 3)) 59 | } 60 | // With a 1/2 chance, negate `r`. 61 | if iszero(and(0x20, d)) { r := not(r) } 62 | break 63 | } 64 | // Otherwise, just set `r` to `xor(sValue, r)`. 65 | r := xor(sValue, r) 66 | break 67 | } 68 | } 69 | } 70 | 71 | /// @dev Returns a random signer and its private key. 72 | function _randomSigner() internal returns (address signer, uint256 privateKey) { 73 | uint256 privateKeyMax = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140; 74 | privateKey = _hem(_random(), 1, privateKeyMax); 75 | /// @solidity memory-safe-assembly 76 | assembly { 77 | mstore(0x00, 0xffa18649) // `addr(uint256)`. 78 | mstore(0x20, privateKey) 79 | if iszero(call(gas(), _VM_ADDRESS, 0, 0x1c, 0x24, 0x00, 0x20)) { revert(0, 0) } 80 | signer := mload(0x00) 81 | } 82 | } 83 | 84 | /// @dev Returns a random address. 85 | function _randomAddress() internal returns (address result) { 86 | result = address(uint160(_random())); 87 | } 88 | 89 | /// @dev Returns a random non-zero address. 90 | function _randomNonZeroAddress() internal returns (address result) { 91 | do { 92 | result = address(uint160(_random())); 93 | } while (result == address(0)); 94 | } 95 | 96 | /// @dev Adapted from `bound`: 97 | /// https://github.com/foundry-rs/forge-std/blob/ff4bf7db008d096ea5a657f2c20516182252a3ed/src/StdUtils.sol#L10 98 | /// Differentially fuzzed tested against the original implementation. 99 | function _hem(uint256 x, uint256 min, uint256 max) internal pure virtual returns (uint256 result) { 100 | require(min <= max, "Max is less than min."); 101 | 102 | /// @solidity memory-safe-assembly 103 | assembly { 104 | // prettier-ignore 105 | for {} 1 {} { 106 | // If `x` is between `min` and `max`, return `x` directly. 107 | // This is to ensure that dictionary values 108 | // do not get shifted if the min is nonzero. 109 | // More info: https://github.com/foundry-rs/forge-std/issues/188 110 | if iszero(or(lt(x, min), gt(x, max))) { 111 | result := x 112 | break 113 | } 114 | 115 | let size := add(sub(max, min), 1) 116 | if and(iszero(gt(x, 3)), gt(size, x)) { 117 | result := add(min, x) 118 | break 119 | } 120 | 121 | let w := not(0) 122 | if and(iszero(lt(x, sub(0, 4))), gt(size, sub(w, x))) { 123 | result := sub(max, sub(w, x)) 124 | break 125 | } 126 | 127 | // Otherwise, wrap x into the range [min, max], 128 | // i.e. the range is inclusive. 129 | if iszero(lt(x, max)) { 130 | let d := sub(x, max) 131 | let r := mod(d, size) 132 | if iszero(r) { 133 | result := max 134 | break 135 | } 136 | result := add(add(min, r), w) 137 | break 138 | } 139 | let d := sub(min, x) 140 | let r := mod(d, size) 141 | if iszero(r) { 142 | result := min 143 | break 144 | } 145 | result := add(sub(max, r), 1) 146 | break 147 | } 148 | } 149 | } 150 | 151 | function min3(uint256 a, uint256 b, uint256 c) internal pure returns (uint256) { 152 | return a > b ? (b > c ? c : b) : (a > c ? c : a); 153 | } 154 | 155 | } 156 | --------------------------------------------------------------------------------