├── abis └── .keep ├── metadata ├── index.html └── assets │ ├── zorb.png │ └── metadata.json ├── .env.example ├── package ├── index.ts └── typedData.ts ├── script ├── config │ ├── claimConfig.json │ ├── claim │ │ ├── 8453.json │ │ └── 84532.json │ ├── deploymentConfig.json │ ├── deterministic │ │ ├── claimBase.json │ │ └── token.json │ └── deterministicConfig.json ├── PrintMintSupplyCall.s.sol ├── TransferToClaim.s.sol ├── DeployClaimNonDeterminisitic.s.sol ├── DeployClaim.s.sol ├── DeployDevelopmentCommunityClaim.s.sol ├── DeployToken.s.sol ├── PrepareForSaltMining.s.sol ├── DeployDevelopmentContracts.s.sol ├── PrepareForSaltMiningToken.s.sol └── PrepareForSaltMiningClaim.s.sol ├── audit └── Zora Token - Zellic Audit Report.pdf ├── src ├── interfaces │ └── IERC7572.sol ├── deployment │ ├── IImmutableCreate2Factory.sol │ └── DeploymentBase.sol ├── utils │ └── UnorderedNonces.sol ├── zora │ ├── IZora.sol │ └── Zora.sol ├── development │ └── DevelopmentCommunityClaim.sol └── claim │ ├── IZoraTokenCommunityClaim.sol │ └── ZoraTokenCommunityClaim.sol ├── slither.config.json ├── remappings.txt ├── addresses ├── 8453.json └── 84532.json ├── tsconfig.build.json ├── .changeset ├── config.json └── README.md ├── .prettierrc ├── tsup.config.ts ├── foundry.toml ├── .gitignore ├── tsconfig.json ├── .github ├── actions │ └── setup_deps │ │ └── action.yml └── workflows │ ├── test.yml │ └── coverage.yml ├── README.md ├── CHANGELOG.md ├── LICENSE ├── docs └── base-token-security-notes.md ├── package.json ├── DEPLOYMENT.md ├── wagmi.config.ts └── test ├── PoolAddresses.t.sol ├── Zora.t.sol ├── development └── DevelopmentCommunityClaim.t.sol └── ZoraTokenCommunityClaim.t.sol /abis/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metadata/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TENDERLY_ACCESS_KEY= 2 | 3 | TENDERLY_VIRTUAL_TESTNET_RPC_URL= -------------------------------------------------------------------------------- /package/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./generated"; 2 | export * from "./typedData"; 3 | -------------------------------------------------------------------------------- /metadata/assets/zorb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalim17/zora-token/HEAD/metadata/assets/zorb.png -------------------------------------------------------------------------------- /metadata/assets/metadata.json: -------------------------------------------------------------------------------- 1 | {"name": "Zora", "description": "coin it", "image": "https://www.theme.wtf/assets/zorb.png"} 2 | -------------------------------------------------------------------------------- /script/config/claimConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin": "0xBEBe537eFb8377629A1dFB1aC5c0568036E32712", 3 | "claimStart": 1743811200 4 | } 5 | -------------------------------------------------------------------------------- /audit/Zora Token - Zellic Audit Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalim17/zora-token/HEAD/audit/Zora Token - Zellic Audit Report.pdf -------------------------------------------------------------------------------- /script/config/claim/8453.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin": "0x078362961fCF3E48Cdc850F9cBCa335d0b47d968", 3 | "allocationSetter": "0xe847eBff4d1B7ae3691f238c5Cd5Cf691Fd1F413" 4 | } 5 | -------------------------------------------------------------------------------- /script/config/claim/84532.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin": "0x5F14C23983c9e0840Dc60dA880349622f0785420", 3 | "allocationSetter": "0xBEBe537eFb8377629A1dFB1aC5c0568036E32712" 4 | } 5 | -------------------------------------------------------------------------------- /src/interfaces/IERC7572.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC7572 { 5 | function contractURI() external view returns (string memory); 6 | } -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter_paths": "(node_modules/,test/)", 3 | "solc_remaps": [ 4 | "@zoralabs/=node_modules/@zoralabs/", 5 | "@openzeppelin/=node_modules/@openzeppelin/" 6 | ] 7 | } -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=node_modules/ds-test/src/ 2 | forge-std/=node_modules/forge-std/src/ 3 | @openzeppelin/=node_modules/@openzeppelin/ 4 | solemate/=/node_modules/solemate/src/ 5 | solady/=node_modules/solady/src/ 6 | -------------------------------------------------------------------------------- /addresses/8453.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEVELOPMENT_COMMUNITY_CLAIM": "0x63A230a6E60f880741df0348C73C9A5f56D71D44", 3 | "ZORA_TOKEN": "0x1111111111166b7FE7bd91427724B487980aFc69", 4 | "ZORA_TOKEN_COMMUNITY_CLAIM": "0x0000000002ba96C69b95E32CAAB8fc38bAB8B3F8" 5 | } 6 | -------------------------------------------------------------------------------- /addresses/84532.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEVELOPMENT_COMMUNITY_CLAIM": "0x2BfC7bc056d51746213F62CeBc16720FFda89F60", 3 | "ZORA_TOKEN": "0xa5EAd88ABb76aa1e582329D7C0ab057511c6Aac2", 4 | "ZORA_TOKEN_COMMUNITY_CLAIM": "0xbaC462F313850b5Eeb88201f7B22FDc8f293C807" 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["es2021", "DOM"], 5 | "baseUrl": ".", 6 | "outDir": "dist" 7 | }, 8 | "exclude": ["node_modules/**", "dist/**"], 9 | "include": ["package/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-solidity"], 3 | "overrides": [ 4 | { 5 | "files": "*.sol", 6 | "options": { 7 | "printWidth": 160, 8 | "tabWidth": 4, 9 | "useTabs": false, 10 | "singleQuote": false, 11 | "bracketSpacing": false 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /script/config/deploymentConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin": "0x078362961fCF3E48Cdc850F9cBCa335d0b47d968", 3 | "contractURI": "https://www.theme.wtf/assets/metadata.json", 4 | "initialMints": [ 5 | { 6 | "name": "zora", 7 | "addr": "0x078362961fCF3E48Cdc850F9cBCa335d0b47d968", 8 | "amount": "10_000_000_000" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["package/index.ts"], 5 | sourcemap: true, 6 | clean: true, 7 | tsconfig: "tsconfig.build.json", 8 | dts: false, 9 | format: ["cjs", "esm"], 10 | onSuccess: 11 | "tsc --project tsconfig.build.json --emitDeclarationOnly --declaration --declarationMap", 12 | }); 13 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["node_modules"] 5 | via_ir = true 6 | solc_version = '0.8.28' 7 | optimizer = true 8 | optimizer_runs = 1_000_000 9 | fs_permissions = [ 10 | { access = "readwrite", path = "./addresses" }, 11 | { access = "readwrite", path = "./script/config" }, 12 | { access = "read", path = "./package.json" }, 13 | ] 14 | 15 | [rpc_endpoints] 16 | base = "${TENDERLY_KEY}" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | abis/*.json 5 | 6 | # Ignores development broadcast logs 7 | broadcast 8 | # /broadcast/*/31337/ 9 | # /broadcast/**/dry-run/ 10 | 11 | # Remove broadcast logs 12 | **/*/broadcast 13 | 14 | # Dotenv file 15 | **/*.env 16 | 17 | # osx 18 | .DS_Store 19 | 20 | # npm 21 | **/*node_modules/ 22 | 23 | # coverage info 24 | lcov.info 25 | 26 | # package files 27 | dist/ 28 | 29 | .env 30 | .env.* 31 | !.env.example 32 | !.env.anvil 33 | 34 | .vercel 35 | 36 | package/generated.ts 37 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "lib": ["es2021"], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "noImplicitAny": true, 11 | "noUncheckedIndexedAccess": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "strictNullChecks": true, 18 | "target": "es2021", 19 | "types": ["node"] 20 | }, 21 | "exclude": ["node_modules/**", "dist/**"], 22 | "include": ["loadTesting/**/*.ts", "scripts/**/*.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /.github/actions/setup_deps/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Node and Foundry" 2 | description: "Setups up node and foundry" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Enable Corepack 7 | run: corepack enable 8 | shell: bash 9 | 10 | - uses: pnpm/action-setup@v4 11 | name: Install pnpm 12 | with: 13 | run_install: false 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 20.10.0 19 | cache: "pnpm" 20 | 21 | - name: Install dependencies 22 | run: pnpm install 23 | shell: bash 24 | 25 | - name: Install Foundry 26 | uses: foundry-rs/foundry-toolchain@v1 27 | with: 28 | version: nightly-6b07c77eb1c1d1c4b56ffa7f79240254b73236d2 -------------------------------------------------------------------------------- /script/PrintMintSupplyCall.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Zora} from "../src/zora/Zora.sol"; 5 | 6 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 7 | import {IZora} from "../src/zora/IZora.sol"; 8 | 9 | import "forge-std/Script.sol"; 10 | 11 | contract DeployToken is DeploymentBase { 12 | error MismatchedAddresses(address expected, address actual); 13 | 14 | function run() public view { 15 | (address initializeFrom, bytes memory initializeCall) = getInitializeCall(); 16 | 17 | // print out instruction for minting the supply 18 | console2.log("Execute the following call to mint the supply:"); 19 | console2.log("Multisig:", initializeFrom); 20 | console2.log("Call:"); 21 | console2.logBytes(initializeCall); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /script/TransferToClaim.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Zora} from "../src/zora/Zora.sol"; 5 | 6 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 7 | import {DevelopmentCommunityClaim} from "../src/development/DevelopmentCommunityClaim.sol"; 8 | 9 | import "forge-std/Script.sol"; 10 | 11 | contract TransferToClaim is DeploymentBase { 12 | error MismatchedAddresses(address expected, address actual); 13 | 14 | function run() public { 15 | AddressesConfig memory addressesConfig = getAddressesConfig(); 16 | 17 | // start broadcast with the admin address 18 | vm.startBroadcast(); 19 | 20 | Zora zora = Zora(addressesConfig.zoraToken); 21 | 22 | zora.transfer(addressesConfig.developmentCommunityClaim, 100_000_000 * 10 ** 18); 23 | 24 | vm.stopBroadcast(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zora Token Contracts 2 | 3 | This repository contains the contracts for the Zora token. 4 | 5 | 6 | ## Contracts 7 | 8 | - [Zora.sol](./src/Zora.sol) - The main Zora token contract. 9 | - [IZora.sol](./src/IZora.sol) - The Zora token contract interface. 10 | 11 | - [ZoraTokenCommunityClaim.sol](./src/claim/ZoraTokenCommunityClaim.sol) - Main community claim contract. 12 | - [IZoraTokenCommunityClaim.sol](./src/claim/IZoraTokenCommunityClaim.sol) - Main community claim contract interface. 13 | 14 | ## Audit Report 15 | 16 | This project has a [Zellic Audit Report](audit%2FZora%20Token%20-%20Zellic%20Audit%20Report.pdf) at ba75438 17 | 18 | ## Setup 19 | 20 | Install dependencies 21 | 22 | ```bash 23 | pnpm install 24 | ``` 25 | 26 | ## Run tests in watch mode 27 | 28 | ```bash 29 | pnpm dev 30 | ``` 31 | 32 | ## Public 33 | 34 | The public webroot for metadata is `metadata/`. Be mindful with files in that directory. 35 | -------------------------------------------------------------------------------- /script/DeployClaimNonDeterminisitic.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 6 | import {ZoraTokenCommunityClaim} from "../src/claim/ZoraTokenCommunityClaim.sol"; 7 | 8 | contract DeployClaim is DeploymentBase { 9 | function run() external { 10 | AddressesConfig memory addressesConfig = getAddressesConfig(); 11 | 12 | ClaimConfig memory claimConfig = getClaimConfig(block.chainid); 13 | 14 | // start broadcast with the admin address 15 | vm.startBroadcast(); 16 | 17 | addressesConfig.zoraTokenCommunityClaim = address( 18 | new ZoraTokenCommunityClaim(claimConfig.allocationSetter, claimConfig.admin, addressesConfig.zoraToken) 19 | ); 20 | 21 | vm.stopBroadcast(); 22 | 23 | saveAddressesConfig(addressesConfig); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @zoralabs/zora-token-contracts 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - 55174b2: Storing allocation amounts, and allowing set allocations with signature 8 | - 1573442: Deployed latest claim 9 | - 793f3db: Combined storage slots for claim contract 10 | 11 | ## 0.1.1 12 | 13 | ### Patch Changes 14 | 15 | - 2328c40: Removed indexed flags from array values in events to make them easier to index 16 | 17 | ## 0.1.0 18 | 19 | ### Minor Changes 20 | 21 | - b3f3c6b: Created dev community claim contract. made base claim contract with logic for if claim is open moved to subclass 22 | 23 | ### Patch Changes 24 | 25 | - 04e00e0: Added expiration details to signature expired error 26 | 27 | ## 0.0.5 28 | 29 | ### Patch Changes 30 | 31 | - 49e37ec: Zora Token Contract dependency updates 32 | 33 | ## 0.0.4 34 | 35 | ### Patch Changes 36 | 37 | - 0a3a586: Saved tenderly virtual testnet deployment 38 | - 0a3a586: Add abis in JSON to package exports 39 | - 0a3a586: Included typed data generation helper 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | 4 | jobs: 5 | check: 6 | strategy: 7 | fail-fast: true 8 | 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set initial test status 13 | uses: myrotvorets/set-commit-status-action@v2.0.0 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | status: pending 17 | context: Test status 18 | 19 | - name: Checkout Repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Install node deps and foundry 23 | uses: ./.github/actions/setup_deps 24 | 25 | - name: Build contracts 26 | run: | 27 | forge build 28 | 29 | - name: Test 30 | run: | 31 | forge test 32 | env: 33 | TENDERLY_KEY: ${{ secrets.TENDERLY_KEY }} 34 | 35 | - name: Set initial test status 36 | uses: myrotvorets/set-commit-status-action@v2.0.0 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | status: ${{ job.status }} 40 | context: Test status 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zora Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | 4 | jobs: 5 | check: 6 | strategy: 7 | fail-fast: true 8 | 9 | name: Coverage 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@v4 14 | 15 | - name: Install node deps and foundry 16 | uses: ./.github/actions/setup_deps 17 | 18 | - name: Run Forge coverage 19 | run: forge coverage --report lcov 20 | env: 21 | TENDERLY_KEY: ${{ secrets.TENDERLY_KEY }} 22 | 23 | - name: Setup LCOV 24 | uses: hrishikesh-kadam/setup-lcov@v1 25 | 26 | - name: Filter files to ignore 27 | run: | 28 | lcov --rc lcov_branch_coverage=1 \ 29 | --remove lcov.info \ 30 | --ignore-errors unused \ 31 | --output-file lcov.info "*test*" "script/*" "src/deployment*" 32 | 33 | - name: Report code coverage 34 | uses: iainnash/github-actions-report-lcov@v12 35 | with: 36 | output-html: false 37 | coverage-files: ./lcov.info 38 | minimum-coverage: 87 39 | github-token: ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /src/deployment/IImmutableCreate2Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | // Extracted interface of ImmutableCreate2Factory.sol, located at https://raw.githubusercontent.com/0age/metamorphic/master/contracts/ImmutableCreate2Factory.sol 5 | 6 | interface IImmutableCreate2Factory { 7 | /** 8 | * @dev Create a contract using CREATE2 by submitting a given salt or nonce 9 | * along with the initialization code for the contract. Note that the first 20 10 | * bytes of the salt must match those of the calling address, which prevents 11 | * contract creation events from being submitted by unintended parties. 12 | * @param salt bytes32 The nonce that will be passed into the CREATE2 call. 13 | * @param initializationCode bytes The initialization code that will be passed 14 | * into the CREATE2 call. 15 | * @return deploymentAddress Address of the contract that will be created, or the null address 16 | * if a contract already exists at that address. 17 | */ 18 | function safeCreate2(bytes32 salt, bytes calldata initializationCode) external payable returns (address deploymentAddress); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/UnorderedNonces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | /** 5 | * @dev Provides tracking nonces for addresses. Nonces can be in any order and just need to be unique. 6 | */ 7 | abstract contract UnorderedNonces { 8 | /** 9 | * @dev The nonce used for an `account` is not the expected current nonce. 10 | */ 11 | error InvalidAccountNonce(address account, bytes32 currentNonce); 12 | 13 | /// @custom:storage-location erc7201:unorderedNonces.storage.UnorderedNoncesStorage 14 | mapping(address account => mapping(bytes32 => bool)) nonces; 15 | 16 | /** 17 | * @dev Returns whether a nonce has been used for an address. 18 | */ 19 | function nonceUsed(address owner, bytes32 nonce) public view virtual returns (bool) { 20 | return nonces[owner][nonce]; 21 | } 22 | 23 | /** 24 | * @dev Same as {_useNonce} but checking that `nonce` passed in is valid. 25 | */ 26 | function _useCheckedNonce(address owner, bytes32 nonce) internal virtual { 27 | if (nonces[owner][nonce]) { 28 | revert InvalidAccountNonce(owner, nonce); 29 | } 30 | nonces[owner][nonce] = true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/zora/IZora.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC7572} from "../interfaces/IERC7572.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | interface IZora is IERC7572, IERC20 { 8 | /// @notice Used once during deploy to mint and allocate the initial and total supply of the ERC20 9 | /// @param tos Addresses to allocate tokens to by the deployer 10 | /// @param amounts Amounts of tokens to allocate alongside the tos address array 11 | /// @param uri Contract URI to set for the token metadata 12 | function initialize(address[] calldata tos, uint256[] calldata amounts, string memory uri) external; 13 | 14 | /// @notice Invalid input lengths when the tos and amounts array lengths do not match 15 | error InvalidInputLengths(); 16 | 17 | /// @notice Error when any user other than the admin attempts to mint the initial and final supply 18 | error OnlyAdmin(); 19 | 20 | /// @notice Error when URI is not provided during initialization 21 | error URINeedsToBeSet(); 22 | 23 | /// @notice Error when a zero address is used as the initializer account 24 | error InitializerCannotBeAddressZero(); 25 | } 26 | -------------------------------------------------------------------------------- /script/DeployClaim.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 6 | import {ZoraTokenCommunityClaim} from "../src/claim/ZoraTokenCommunityClaim.sol"; 7 | 8 | contract DeployClaim is DeploymentBase { 9 | function run() external { 10 | vm.startBroadcast(); 11 | 12 | AddressesConfig memory addressesConfig = getAddressesConfig(); 13 | 14 | DeterministicConfig memory claimDeterministicConfig = getDeterministicConfig(ZORA_TOKEN_COMMUNITY_CLAIM_BASE_CONTRACT_NAME); 15 | 16 | addressesConfig.zoraTokenCommunityClaim = getImmutableCreate2Factory().safeCreate2( 17 | claimDeterministicConfig.salt, 18 | claimDeterministicConfig.creationCode 19 | ); 20 | 21 | require(addressesConfig.zoraTokenCommunityClaim == claimDeterministicConfig.expectedAddress, "Mismatched addresses"); 22 | 23 | ZoraTokenCommunityClaim claim = ZoraTokenCommunityClaim(addressesConfig.zoraTokenCommunityClaim); 24 | 25 | ClaimConfig memory claimConfig = getClaimConfig(block.chainid); 26 | 27 | require(claim.admin() == claimConfig.admin, "Mismatched admin"); 28 | require(claim.allocationSetter() == claimConfig.allocationSetter, "Mismatched allocation setter"); 29 | 30 | saveAddressesConfig(addressesConfig); 31 | 32 | vm.stopBroadcast(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /script/DeployDevelopmentCommunityClaim.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Zora} from "../src/zora/Zora.sol"; 5 | 6 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 7 | import {DevelopmentCommunityClaim} from "../src/development/DevelopmentCommunityClaim.sol"; 8 | import {ZoraTokenCommunityClaim} from "../src/claim/ZoraTokenCommunityClaim.sol"; 9 | import "forge-std/Script.sol"; 10 | 11 | contract DeployDevelopmentCommunityClaim is DeploymentBase { 12 | error MismatchedAddresses(address expected, address actual); 13 | 14 | function run() public { 15 | AddressesConfig memory addressesConfig = getAddressesConfig(); 16 | 17 | ClaimConfig memory claimConfig = getClaimConfig(block.chainid); 18 | 19 | // start broadcast with the admin address 20 | vm.startBroadcast(); 21 | 22 | uint256 claimStart = block.timestamp + 1 hours; // 1 hour from now 23 | 24 | addressesConfig.developmentCommunityClaim = address( 25 | new DevelopmentCommunityClaim(claimConfig.allocationSetter, claimConfig.admin, claimStart, addressesConfig.zoraToken) 26 | ); 27 | 28 | addressesConfig.zoraTokenCommunityClaim = address( 29 | new ZoraTokenCommunityClaim(claimConfig.allocationSetter, claimConfig.admin, addressesConfig.zoraToken) 30 | ); 31 | 32 | vm.stopBroadcast(); 33 | 34 | saveAddressesConfig(addressesConfig); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /script/DeployToken.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Zora} from "../src/zora/Zora.sol"; 5 | 6 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 7 | import {IZora} from "../src/zora/IZora.sol"; 8 | 9 | import "forge-std/Script.sol"; 10 | 11 | contract DeployToken is DeploymentBase { 12 | error MismatchedAddresses(address expected, address actual); 13 | 14 | function run() public { 15 | DeterministicConfig memory deterministicConfig = getDeterministicConfig(ZORA_TOKEN_CONTRACT_NAME); 16 | 17 | AddressesConfig memory addressesConfig = getAddressesConfig(); 18 | 19 | vm.startBroadcast(); 20 | // deploy the zora token contract and call the initialize function 21 | // can only be called by the deployer 22 | address zoraToken = getImmutableCreate2Factory().safeCreate2(deterministicConfig.salt, deterministicConfig.creationCode); 23 | 24 | addressesConfig.zoraToken = zoraToken; 25 | 26 | require(zoraToken == deterministicConfig.expectedAddress, MismatchedAddresses(deterministicConfig.expectedAddress, zoraToken)); 27 | 28 | vm.stopBroadcast(); 29 | 30 | (address initializeFrom, bytes memory initializeCall) = getInitializeCall(); 31 | 32 | saveAddressesConfig(addressesConfig); 33 | 34 | // print out instruction for minting the supply 35 | console2.log("Execute the following call to mint the supply:"); 36 | console2.log("Multisig:", initializeFrom); 37 | console2.log("Call:"); 38 | console2.logBytes(initializeCall); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/base-token-security-notes.md: -------------------------------------------------------------------------------- 1 | # Base Deployment & Security Notes for ZORA Token 2 | 3 | The ZORA token contract already follows a clear, minimal structure, but a few additional notes could make Base deployments even smoother. 4 | 5 | --- 6 | 7 | ### 1. Deployment Configuration 8 | - Verify that the deployment scripts include Base Mainnet (`chainId: 8453`) and Base Sepolia (`chainId: 84532`). 9 | - Check for RPC fallback logic in the Hardhat or Foundry config to handle temporary endpoint downtime. 10 | - Always validate constructor arguments before deployment on BaseScan. 11 | 12 | --- 13 | 14 | ### 2. Security Checklist 15 | - ✅ Confirm ownership transfer after deployment 16 | - ✅ Verify source code on BaseScan (flattened if needed) 17 | - ✅ Add a time-locked admin or multisig for critical functions 18 | - ✅ Audit minting logic if modified for Base-native tokens 19 | 20 | --- 21 | 22 | ### 3. Testing on Base 23 | Using Base Sepolia for pre-launch testing helps ensure: 24 | - Gas usage remains predictable under L2 compression 25 | - Event logs sync properly for indexers 26 | - Bridge-compatible metadata is preserved 27 | 28 | --- 29 | 30 | ### 4. Transparency 31 | A short section in the README linking to BaseScan contract addresses would increase visibility for the community. 32 | Developers can easily cross-check and reuse verified contract data. 33 | 34 | --- 35 | 36 | ### Final Note 37 | ZORA Token’s design is already elegant. 38 | Adding Base-specific deployment and audit references ensures safer, smoother adoption across the ecosystem. 39 | 40 | > Simplicity scales — and Base is where it shines first. 41 | -------------------------------------------------------------------------------- /src/development/DevelopmentCommunityClaim.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.28; 3 | 4 | import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; 5 | import {IZoraTokenCommunityClaim} from "../claim/IZoraTokenCommunityClaim.sol"; 6 | import {ZoraTokenCommunityClaim} from "../claim/ZoraTokenCommunityClaim.sol"; 7 | 8 | /// Similar to ZoraTokenCommunityClaim but adds useful functions for development purposes: 9 | /// Allows setting the claim start time even after claim has started 10 | /// Allows resetting if a user has claimed; allowing a user to claim multiple times 11 | /// Allowing setting of allocations regardless of whether the claim has started or setup is complete 12 | contract DevelopmentCommunityClaim is ZoraTokenCommunityClaim { 13 | // Compact allocations stored as uint96 (token count, will be multiplied by 1e18) 14 | constructor(address _allocationSetter, address _admin, uint256 _claimStart, address _token) ZoraTokenCommunityClaim(_allocationSetter, _admin, _token) { 15 | claimStart = _claimStart; 16 | } 17 | 18 | // override - allows to set allocations even after claim has started 19 | function _checkCanSetAllocations(address _sender) internal view override { 20 | require(_sender == allocationSetter, OnlyAllocationSetter()); 21 | } 22 | 23 | function _checkCanClaim() internal view override { 24 | // override - allows to claim even before claim has started 25 | } 26 | 27 | // special function to set the claim start time only available on the dev contract 28 | function _checkCanUpdateClaimStart(uint256) internal view override { 29 | require(msg.sender == admin, OnlyAdmin()); 30 | // override - allows to update claim start even after claim has started 31 | } 32 | 33 | event ResetHasClaimed(address[] users); 34 | 35 | function resetHasClaimed(address[] calldata _users) external { 36 | require(msg.sender == allocationSetter, OnlyAllocationSetter()); 37 | for (uint256 i = 0; i < _users.length; i++) { 38 | accountClaims[_users[i]].claimed = false; 39 | } 40 | 41 | emit ResetHasClaimed(_users); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /script/PrepareForSaltMining.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {IZora, Zora} from "../src/zora/Zora.sol"; 5 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 6 | import {LibString} from "solady/utils/LibString.sol"; 7 | 8 | import "forge-std/Script.sol"; 9 | 10 | contract MineDeterministicConfig is DeploymentBase { 11 | function printMintSaltCommand(address deployer, bytes32 initCodeHash, string memory startsWith, address caller) internal pure { 12 | string memory command = string.concat( 13 | "cast create2 --starts-with ", 14 | startsWith, 15 | " --deployer ", 16 | LibString.toHexString(deployer), 17 | " --init-code-hash ", 18 | LibString.toHexStringNoPrefix(uint256(initCodeHash), 32), 19 | " --caller ", 20 | LibString.toHexString(caller) 21 | ); 22 | console2.log(command); 23 | } 24 | 25 | function run() public { 26 | // first we need to get the salt and deterministic config for the deployerAndCaller contract. 27 | // we pass to its constructor the deployer address, which means that only the deployer can use that contract 28 | // to deploy and call other contracts. 29 | address deployer = getDeploymentConfig().admin; 30 | 31 | // now we mine for the salt and expected address for the zora token 32 | // when creating the zora token, there are no constructor args...the msg.sender is the caller. 33 | bytes memory tokenCreationCode = abi.encodePacked(type(Zora).creationCode, abi.encode(deployer)); 34 | 35 | console2.log("Execute this command to mine the salt for the zora token contract:"); 36 | printMintSaltCommand(IMMUTABLE_CREATE2_FACTORY_ADDRESS, keccak256(tokenCreationCode), "1111111", address(0)); 37 | console2.log("Once this is done, update the deterministicConfig.json file with the new salt and expected address."); 38 | 39 | DeterministicConfig memory deterministicConfig; 40 | deterministicConfig.creationCode = tokenCreationCode; 41 | 42 | //saveDeterministicConfig(deterministicConfig); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zoralabs/zora-token-contracts", 3 | "version": "0.2.1", 4 | "packageManager": "pnpm@9.6.0", 5 | "license": "MIT", 6 | "type": "module", 7 | "main": "./dist/index.cjs", 8 | "module": "./dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "sideEffects": false, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "exports": { 15 | ".": { 16 | "types": "./dist/index.d.ts", 17 | "import": "./dist/index.js", 18 | "default": "./dist/index.cjs" 19 | } 20 | }, 21 | "files": [ 22 | "dist", 23 | "abis", 24 | "package", 25 | "README.md", 26 | "LICENSE" 27 | ], 28 | "scripts": { 29 | "prettier:check": "prettier --check 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", 30 | "prettier:write": "prettier --write 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", 31 | "test": "forge test -vv", 32 | "dev": "forge test --watch -vvv", 33 | "test-gas": "forge test --gas-report", 34 | "build:sizes": "forge build src/ --sizes", 35 | "copy-abis": "pnpm node scripts/package/copy-abis.js", 36 | "prepack": "pnpm build && pnpm copy-abis", 37 | "coverage": "forge coverage --report lcov", 38 | "build": "pnpm run wagmi:generate && tsup", 39 | "wagmi:generate": "wagmi generate", 40 | "update-contract-version": "pnpm exec update-contract-version" 41 | }, 42 | "devDependencies": { 43 | "@changesets/cli": "^2.28.1", 44 | "@google-cloud/bigquery": "^7.9.3", 45 | "@openzeppelin/contracts": "5.0.2", 46 | "@openzeppelin/contracts-upgradeable": "5.0.2", 47 | "@solidity-parser/parser": "0.19.0", 48 | "@types/node": "^22.13.10", 49 | "@wagmi/cli": "^2.2.0", 50 | "axios": "^1.8.1", 51 | "csv-parse": "^5.6.0", 52 | "ds-test": "https://github.com/dapphub/ds-test#cd98eff28324bfac652e63a239a60632a761790b", 53 | "forge-std": "https://github.com/foundry-rs/forge-std#v1.9.1", 54 | "prettier": "^3.0.3", 55 | "prettier-plugin-solidity": "^1.4.1", 56 | "solady": "^0.1.1", 57 | "solmate": "^6.8.0", 58 | "tsup": "^8.4.0", 59 | "viem": "^2.23.5" 60 | }, 61 | "dependencies": { 62 | "mongodb": "^6.15.0", 63 | "typescript": "^5.8.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /script/DeployDevelopmentContracts.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Zora} from "../src/zora/Zora.sol"; 5 | 6 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 7 | import {DevelopmentCommunityClaim} from "../src/development/DevelopmentCommunityClaim.sol"; 8 | import {ZoraTokenCommunityClaim} from "../src/claim/ZoraTokenCommunityClaim.sol"; 9 | import "forge-std/Script.sol"; 10 | 11 | contract DeployDevelopmentContracts is DeploymentBase { 12 | error MismatchedAddresses(address expected, address actual); 13 | 14 | function run() public { 15 | AddressesConfig memory addressesConfig = getAddressesConfig(); 16 | 17 | ClaimConfig memory claimConfig = getClaimConfig(block.chainid); 18 | 19 | // start broadcast with the admin address 20 | vm.startBroadcast(); 21 | 22 | Zora zora = new Zora(claimConfig.allocationSetter); 23 | 24 | // deploy the zora token contract and call the initialize function 25 | // can only be called by the deployer 26 | addressesConfig.zoraToken = address(zora); 27 | 28 | uint256 claimStart = block.timestamp + 1 minutes; // 1 minute from now 29 | 30 | addressesConfig.developmentCommunityClaim = address( 31 | new DevelopmentCommunityClaim(claimConfig.allocationSetter, claimConfig.admin, claimStart, addressesConfig.zoraToken) 32 | ); 33 | 34 | addressesConfig.zoraTokenCommunityClaim = address( 35 | new ZoraTokenCommunityClaim(claimConfig.allocationSetter, claimConfig.admin, addressesConfig.zoraToken) 36 | ); 37 | 38 | address[] memory users = new address[](2); 39 | uint256[] memory amounts = new uint256[](2); 40 | // give half to the allocation setter 41 | users[0] = claimConfig.allocationSetter; 42 | amounts[0] = 5_000_000_000 * 1e18; 43 | // give half to the admin 44 | users[1] = claimConfig.admin; 45 | amounts[1] = 5_000_000_000 * 1e18; 46 | 47 | zora.initialize(users, amounts, "https://www.theme.wtf/assets/metadata.json"); 48 | 49 | vm.stopBroadcast(); 50 | 51 | saveAddressesConfig(addressesConfig); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /script/PrepareForSaltMiningToken.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {IZora, Zora} from "../src/zora/Zora.sol"; 5 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 6 | import {LibString} from "solady/utils/LibString.sol"; 7 | 8 | import "forge-std/Script.sol"; 9 | 10 | contract MineDeterministicConfig is DeploymentBase { 11 | function printMintSaltCommand(address deployer, bytes32 initCodeHash, string memory startsWith, address caller) internal pure { 12 | string memory command = string.concat( 13 | "cargo run --release -- --starts-with ", 14 | startsWith, 15 | " --deployer ", 16 | LibString.toHexString(deployer), 17 | " --init-code-hash ", 18 | LibString.toHexStringNoPrefix(uint256(initCodeHash), 32), 19 | " --caller ", 20 | LibString.toHexString(caller) 21 | ); 22 | console2.log(command); 23 | } 24 | 25 | function run() public { 26 | // first we need to get the salt and deterministic config for the deployerAndCaller contract. 27 | // we pass to its constructor the deployer address, which means that only the deployer can use that contract 28 | // to deploy and call other contracts. 29 | address deployer = getDeploymentConfig().admin; 30 | 31 | // now we mine for the salt and expected address for the zora token 32 | // when creating the zora token, there are no constructor args...the msg.sender is the caller. 33 | bytes memory tokenCreationCode = abi.encodePacked(type(Zora).creationCode, abi.encode(deployer)); 34 | 35 | console2.log("Execute this command to mine the salt for the zora token contract:"); 36 | printMintSaltCommand(IMMUTABLE_CREATE2_FACTORY_ADDRESS, keccak256(tokenCreationCode), "11111111111", address(0)); 37 | console2.log("Once this is done, update the deterministicConfig.json file with the new salt and expected address."); 38 | 39 | DeterministicConfig memory deterministicConfig; 40 | deterministicConfig.creationCode = tokenCreationCode; 41 | 42 | saveDeterministicConfig(deterministicConfig, ZORA_TOKEN_CONTRACT_NAME); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Zora Token Deterministic Deployment 2 | 3 | The Zora token is deployed deterministically using the ImmutableCreate2Factory contract, which allows us to deploy a contract from an EOA at an expected address based on a salt. The minter, which is the account that mints the full supply, is set in the constructor. Anyone can deploy the contract, but only the minter can mint the initial supply. 4 | 5 | The salt for an expected address can be mined using `cast create2`. There are some scripts to help with this process. 6 | 7 | ## 1. Preparing the Config files for Deployment 8 | 9 | Configure `minter` and `initialMints` in [script/config/deploymentConfig.json](script/config/deploymentConfig.json): 10 | 11 | - `minter` - account that will mint the entire supply in a single call. This should be a multisig. 12 | - `initialMints` - array of initial mints to be made on the token. 13 | 14 | ## 2. Mining the Salt 15 | 16 | Once this is configured, we mine the salt for the deployment. The bytecode of the contract is affected by the minter address as it is passed in to the constructor, so make sure that is configured above: 17 | 18 | ```bash 19 | forge script script/PrepareForSaltMining.s.sol $(chains base --rpc) 20 | ``` 21 | 22 | This will print out a command to run to mine the salt. Follow the instructions to mine the salt, then update [script/config/deterministicConfig.json](script/config/deterministicConfig.json) with the new salt and expected address. 23 | 24 | ## 3. Deploying the Token 25 | 26 | Once the salt is mined and configured, any account can deploy the token, by executing the script: 27 | 28 | ```bash 29 | forge script script/DeployToken.s.sol $(chains base --deploy) --broadcast --verify 30 | ``` 31 | 32 | The above command will print out the call to mint the supply, which must be executed by the multisig configured as the `minter` in [script/config/deploymentConfig.json](script/config/deploymentConfig.json). The multisig's address is also printed out. 33 | 34 | ## 4. Minting the Supply 35 | 36 | The mint supply call was printed in the previous step. If it is needed again, it can be printed out by running: 37 | 38 | ```bash 39 | forge script script/PrintInitializeCall.s.sol $(chains base --rpc) 40 | ``` 41 | 42 | The call must be executed by the multisig. 43 | -------------------------------------------------------------------------------- /wagmi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@wagmi/cli"; 2 | import { foundry } from "@wagmi/cli/plugins"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | 6 | // Define the type for addresses 7 | interface AddressData { 8 | ZORA_TOKEN?: string; 9 | ZORA_TOKEN_COMMUNITY_CLAIM?: string; 10 | DEVELOPMENT_COMMUNITY_CLAIM?: string; 11 | [key: string]: string | undefined; 12 | } 13 | 14 | // Read all address files from the addresses directory 15 | const addressesDir = path.join(__dirname, "addresses"); 16 | const addressFiles = fs 17 | .readdirSync(addressesDir) 18 | .filter((file) => file.endsWith(".json")); 19 | 20 | // Create a mapping of chainIds to addresses 21 | const chainAddresses: Record = {}; 22 | 23 | // Process each address file 24 | addressFiles.forEach((file) => { 25 | // Extract chainId from filename (e.g., "8453.json" -> 8453) 26 | const chainId = parseInt(path.basename(file, ".json")); 27 | 28 | // Skip if not a valid number 29 | if (isNaN(chainId)) return; 30 | 31 | // Read and parse the addresses file 32 | const filePath = path.join(addressesDir, file); 33 | const addresses = JSON.parse( 34 | fs.readFileSync(filePath, "utf8"), 35 | ) as AddressData; 36 | 37 | // Store addresses for this chain 38 | chainAddresses[chainId] = addresses; 39 | }); 40 | 41 | // Create deployments configuration 42 | const deployments = { 43 | ZoraTokenCommunityClaim: {}, 44 | Zora: {}, 45 | DevelopmentCommunityClaim: {}, 46 | }; 47 | 48 | // Populate deployments with addresses from all chains 49 | Object.entries(chainAddresses).forEach(([chainId, addresses]) => { 50 | const numericChainId = parseInt(chainId); 51 | 52 | if (addresses.ZORA_TOKEN_COMMUNITY_CLAIM) { 53 | deployments.ZoraTokenCommunityClaim[numericChainId] = 54 | addresses.ZORA_TOKEN_COMMUNITY_CLAIM; 55 | } 56 | 57 | if (addresses.ZORA_TOKEN) { 58 | deployments.Zora[numericChainId] = addresses.ZORA_TOKEN; 59 | } 60 | 61 | if (addresses.DEVELOPMENT_COMMUNITY_CLAIM) { 62 | deployments.DevelopmentCommunityClaim[numericChainId] = 63 | addresses.DEVELOPMENT_COMMUNITY_CLAIM; 64 | } 65 | }); 66 | 67 | export default defineConfig({ 68 | out: "package/generated.ts", 69 | contracts: [], 70 | plugins: [ 71 | foundry({ 72 | include: ["Zora*", "DevelopmentCommunityClaim*"], 73 | deployments: deployments, 74 | }), 75 | ], 76 | }); 77 | -------------------------------------------------------------------------------- /script/PrepareForSaltMiningClaim.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 5 | import {LibString} from "solady/utils/LibString.sol"; 6 | import {ZoraTokenCommunityClaim} from "../src/claim/ZoraTokenCommunityClaim.sol"; 7 | 8 | import "forge-std/Script.sol"; 9 | 10 | contract MineDeterministicConfig is DeploymentBase { 11 | function printMintSaltCommand(address deployer, bytes32 initCodeHash, string memory startsWith, address caller) internal pure { 12 | string memory command = string.concat( 13 | "cargo run --release -- --starts-with ", 14 | startsWith, 15 | " --deployer ", 16 | LibString.toHexString(deployer), 17 | " --init-code-hash ", 18 | LibString.toHexStringNoPrefix(uint256(initCodeHash), 32), 19 | " --caller ", 20 | LibString.toHexString(caller) 21 | ); 22 | console2.log(command); 23 | } 24 | 25 | function run() public { 26 | // first we need to get the salt and deterministic config for the deployerAndCaller contract. 27 | // we pass to its constructor the deployer address, which means that only the deployer can use that contract 28 | // to deploy and call other contracts. 29 | ClaimConfig memory claimConfig = getClaimConfig(block.chainid); 30 | 31 | DeterministicConfig memory tokenDeterministicConfig = getDeterministicConfig(ZORA_TOKEN_CONTRACT_NAME); 32 | 33 | // now we mine for the salt and expected address for the zora token 34 | // when creating the zora token, there are no constructor args...the msg.sender is the caller. 35 | bytes memory tokenCreationCode = abi.encodePacked( 36 | type(ZoraTokenCommunityClaim).creationCode, 37 | abi.encode(claimConfig.allocationSetter, claimConfig.admin, tokenDeterministicConfig.expectedAddress) 38 | ); 39 | 40 | console2.log("Execute this command to mine the salt for the zora token contract:"); 41 | printMintSaltCommand(IMMUTABLE_CREATE2_FACTORY_ADDRESS, keccak256(tokenCreationCode), "0000000000", address(0)); 42 | 43 | DeterministicConfig memory deterministicConfig; 44 | deterministicConfig.creationCode = tokenCreationCode; 45 | 46 | saveDeterministicConfig(deterministicConfig, ZORA_TOKEN_COMMUNITY_CLAIM_BASE_CONTRACT_NAME); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package/typedData.ts: -------------------------------------------------------------------------------- 1 | import { Address, TypedDataDefinition } from "viem"; 2 | 3 | const TypedDataDomain = "ZoraTokenCommunityClaim"; 4 | const TypedDataVersion = "1"; 5 | 6 | type PermitClaimTypedDataDefinition = TypedDataDefinition< 7 | { 8 | ClaimWithSignature: [ 9 | { name: "user"; type: "address" }, 10 | { name: "claimTo"; type: "address" }, 11 | { name: "deadline"; type: "uint256" }, 12 | ]; 13 | }, 14 | "ClaimWithSignature" 15 | >; 16 | // typed data domain: 17 | export const permitClaimTypedDataDefinition = ({ 18 | message, 19 | chainId, 20 | claimContract, 21 | }: { 22 | message: { 23 | user: Address; 24 | claimTo: Address; 25 | deadline: bigint; 26 | }; 27 | chainId: number; 28 | claimContract: Address; 29 | }) => { 30 | const typesAndMessage: Omit = { 31 | types: { 32 | ClaimWithSignature: [ 33 | { name: "user", type: "address" }, 34 | { name: "claimTo", type: "address" }, 35 | { name: "deadline", type: "uint256" }, 36 | ], 37 | }, 38 | message, 39 | primaryType: "ClaimWithSignature", 40 | }; 41 | 42 | return { 43 | ...typesAndMessage, 44 | domain: { 45 | chainId, 46 | name: TypedDataDomain, 47 | version: TypedDataVersion, 48 | verifyingContract: claimContract, 49 | }, 50 | }; 51 | }; 52 | 53 | type SetAllocationsTypedDataDefinition = TypedDataDefinition< 54 | { 55 | SetAllocations: [ 56 | { name: "packedData"; type: "bytes32[]" }, 57 | { name: "nonce"; type: "bytes32" }, 58 | ]; 59 | }, 60 | "SetAllocations" 61 | >; 62 | 63 | export const setAllocationsTypedDataDefinition = ({ 64 | message, 65 | chainId, 66 | claimContract, 67 | }: { 68 | message: { 69 | packedData: `0x${string}`[]; 70 | nonce: `0x${string}`; 71 | }; 72 | chainId: number; 73 | claimContract: Address; 74 | }) => { 75 | const typesAndMessage: Omit = { 76 | types: { 77 | SetAllocations: [ 78 | { name: "packedData", type: "bytes32[]" }, 79 | { name: "nonce", type: "bytes32" }, 80 | ], 81 | }, 82 | message, 83 | primaryType: "SetAllocations", 84 | }; 85 | 86 | return { 87 | ...typesAndMessage, 88 | domain: { 89 | chainId, 90 | name: TypedDataDomain, 91 | version: TypedDataVersion, 92 | verifyingContract: claimContract, 93 | }, 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /test/PoolAddresses.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | 6 | import {Zora} from "../src/zora/Zora.sol"; 7 | import {IZora} from "../src/zora/IZora.sol"; 8 | import {Test} from "forge-std/Test.sol"; 9 | import {IImmutableCreate2Factory} from "../src/deployment/IImmutableCreate2Factory.sol"; 10 | import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; 11 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 12 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 13 | 14 | /// @title Provides functions for deriving a pool address from the factory, tokens, and the fee 15 | library PoolAddress { 16 | bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; 17 | 18 | /// @notice The identifying key of the pool 19 | struct PoolKey { 20 | address token0; 21 | address token1; 22 | uint24 fee; 23 | } 24 | 25 | /// @notice Returns PoolKey: the ordered tokens with the matched fee levels 26 | /// @param tokenA The first token of a pool, unsorted 27 | /// @param tokenB The second token of a pool, unsorted 28 | /// @param fee The fee level of the pool 29 | /// @return Poolkey The pool details with ordered token0 and token1 assignments 30 | function getPoolKey(address tokenA, address tokenB, uint24 fee) internal pure returns (PoolKey memory) { 31 | if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); 32 | return PoolKey({token0: tokenA, token1: tokenB, fee: fee}); 33 | } 34 | 35 | /// @notice Deterministically computes the pool address given the factory and PoolKey 36 | /// @param factory The Uniswap V3 factory contract address 37 | /// @param key The PoolKey 38 | /// @return pool The contract address of the V3 pool 39 | function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) { 40 | require(key.token0 < key.token1); 41 | pool = address( 42 | uint160(uint256(keccak256(abi.encodePacked(hex"ff", factory, keccak256(abi.encode(key.token0, key.token1, key.fee)), POOL_INIT_CODE_HASH)))) 43 | ); 44 | } 45 | } 46 | 47 | interface IUniswapV3Factory { 48 | function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool); 49 | } 50 | 51 | contract PoolAddressesTest is Test { 52 | function testPoolAddresses() public { 53 | vm.createSelectFork("base", 26990278); 54 | 55 | address factory = 0x33128a8fC17869897dcE68Ed026d694621f6FDfD; 56 | 57 | address zora = 0x1111111111166b7FE7bd91427724B487980aFc69; 58 | address weth = 0x4200000000000000000000000000000000000006; 59 | address usdc = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; 60 | 61 | // 1000 = 0.1% 62 | // 10000 = 1% 63 | uint24 fee = 10000; 64 | 65 | PoolAddress.PoolKey memory zoraWeth = PoolAddress.PoolKey({token0: zora, token1: weth, fee: fee}); 66 | address expectedWethPool = PoolAddress.computeAddress(factory, zoraWeth); 67 | PoolAddress.PoolKey memory zoraUsdc = PoolAddress.PoolKey({token0: zora, token1: usdc, fee: fee}); 68 | address expectedUsdcPool = PoolAddress.computeAddress(factory, zoraUsdc); 69 | 70 | address actualWethPool = IUniswapV3Factory(factory).createPool(zora, weth, fee); 71 | address actualUsdcPool = IUniswapV3Factory(factory).createPool(zora, usdc, fee); 72 | 73 | assertEq(actualWethPool, expectedWethPool); 74 | assertEq(actualUsdcPool, expectedUsdcPool); 75 | 76 | console.log("zoraWethPool", expectedWethPool); 77 | console.log("zoraUsdcPool", expectedUsdcPool); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/Zora.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Zora} from "../src/zora/Zora.sol"; 5 | import {IZora} from "../src/zora/IZora.sol"; 6 | import {Test} from "forge-std/Test.sol"; 7 | import {IImmutableCreate2Factory} from "../src/deployment/IImmutableCreate2Factory.sol"; 8 | import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; 9 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 10 | import {DeploymentBase} from "../src/deployment/DeploymentBase.sol"; 11 | 12 | contract ZoraTest is Test, DeploymentBase { 13 | address deployer = makeAddr("deployer"); 14 | 15 | // Get reference to the Immutable Create2 Factory contract 16 | IImmutableCreate2Factory IMMUTABLE_CREATE2_FACTORY = IImmutableCreate2Factory(IMMUTABLE_CREATE2_FACTORY_ADDRESS); 17 | 18 | function testRevertsIfAdminAddressZero() public { 19 | vm.expectRevert(IZora.InitializerCannotBeAddressZero.selector); 20 | new Zora(address(0)); 21 | } 22 | 23 | function testCanMintInitialDistribution() public { 24 | address[] memory tos = new address[](2); 25 | uint256[] memory amounts = new uint256[](2); 26 | 27 | tos[0] = makeAddr("to1"); 28 | tos[1] = makeAddr("to2"); 29 | 30 | amounts[0] = 10 * 10 ** 18; 31 | amounts[1] = 50 * 10 ** 18; 32 | 33 | vm.startPrank(deployer); 34 | Zora zora = new Zora(deployer); 35 | zora.initialize(tos, amounts, "testing"); 36 | 37 | assertEq(zora.balanceOf(tos[0]), amounts[0]); 38 | assertEq(zora.balanceOf(tos[1]), amounts[1]); 39 | } 40 | 41 | function testCannotInitializeAfterAllSupplyIsMinted() public { 42 | address[] memory tos = new address[](1); 43 | uint256[] memory amounts = new uint256[](1); 44 | 45 | tos[0] = makeAddr("to1"); 46 | 47 | amounts[0] = 10 * 10 ** 18; 48 | 49 | Zora zora = new Zora(deployer); 50 | vm.startPrank(deployer); 51 | zora.initialize(tos, amounts, "testing"); 52 | 53 | vm.expectRevert(abi.encodeWithSignature("InvalidInitialization()")); 54 | zora.initialize(new address[](0), new uint256[](0), ""); 55 | } 56 | 57 | function testCannotMintIfNotDeployer() public { 58 | address[] memory tos = new address[](1); 59 | uint256[] memory amounts = new uint256[](1); 60 | 61 | tos[0] = makeAddr("to1"); 62 | amounts[0] = 10 * 10 ** 18; 63 | 64 | Zora zora = new Zora(deployer); 65 | 66 | vm.prank(makeAddr("not-deployer")); 67 | vm.expectRevert(IZora.OnlyAdmin.selector); 68 | zora.initialize(tos, amounts, ""); 69 | } 70 | 71 | function testCanDeterministicallyDeployAtAddress() public { 72 | // Fork Base network so we have the Immutable Create2 Factory deployed 73 | vm.createSelectFork("base", 25695331); 74 | 75 | // Create a deterministic salt by combining the factory deployer's address with a suffix 76 | bytes32 zoraSalt = saltWithAddressInFirst20Bytes(address(0), 3); 77 | 78 | bytes memory zoraCreationCode = abi.encodePacked(type(Zora).creationCode, abi.encode(deployer)); 79 | // Deploy the Zora contract using CREATE2 80 | address zora = IMMUTABLE_CREATE2_FACTORY.safeCreate2(zoraSalt, zoraCreationCode); 81 | 82 | // Verify the deployed address matches the predicted address 83 | address expectedAddress = Create2.computeAddress(zoraSalt, keccak256(zoraCreationCode), IMMUTABLE_CREATE2_FACTORY_ADDRESS); 84 | 85 | assertEq(zora, expectedAddress); 86 | 87 | // Set up initial token distribution parameters 88 | address[] memory tos = new address[](2); 89 | uint256[] memory amounts = new uint256[](2); 90 | tos[0] = makeAddr("to1"); 91 | tos[1] = makeAddr("to2"); 92 | amounts[0] = 10 * 10 ** 18; 93 | amounts[1] = 50 * 10 ** 18; 94 | 95 | // Encode the initial initialize call for the Zora token 96 | 97 | // Test access control: factory deployer cannot deploy Zora token 98 | vm.expectRevert(IZora.OnlyAdmin.selector); 99 | IZora(zora).initialize(tos, amounts, "uri"); 100 | 101 | // Deploy Zora token with initial distribution using the authorized deployer 102 | vm.prank(deployer); 103 | IZora(zora).initialize(tos, amounts, "uri"); 104 | 105 | // Verify initial token distribution was successful 106 | assertEq(IERC20(zora).balanceOf(tos[0]), amounts[0]); 107 | assertEq(IERC20(zora).balanceOf(tos[1]), amounts[1]); 108 | } 109 | 110 | function saltWithAddressInFirst20Bytes(address addressToMakeSaltWith, uint256 suffix) internal pure returns (bytes32) { 111 | uint256 shifted = uint256(uint160(address(addressToMakeSaltWith))) << 96; 112 | 113 | // shifted on the left, suffix on the right: 114 | 115 | return bytes32(shifted | suffix); 116 | } 117 | 118 | function testContractURI() public { 119 | Zora zora = new Zora(deployer); 120 | vm.assertEq(zora.contractURI(), ""); 121 | vm.prank(deployer); 122 | string memory uri = "uri://"; 123 | address[] memory tos = new address[](0); 124 | uint256[] memory amounts = new uint256[](0); 125 | zora.initialize(tos, amounts, uri); 126 | vm.assertEq(zora.contractURI(), "uri://"); 127 | } 128 | 129 | function testDeterministicDeploy() public { 130 | vm.createSelectFork("base", 26943428); 131 | DeterministicConfig memory deterministicConfig = getDeterministicConfig(ZORA_TOKEN_CONTRACT_NAME); 132 | // test that the deployed address is correct 133 | address deployedAddress = IImmutableCreate2Factory(IMMUTABLE_CREATE2_FACTORY_ADDRESS).safeCreate2( 134 | deterministicConfig.salt, 135 | deterministicConfig.creationCode 136 | ); 137 | assertEq(deployedAddress, deterministicConfig.expectedAddress); 138 | 139 | // test that the configured admin can initialize the token 140 | (address initializeFrom, bytes memory initializeCall) = getInitializeCall(); 141 | 142 | // cannot initialize if not the admin 143 | (bool success, ) = deployedAddress.call(initializeCall); 144 | assertEq(success, false); 145 | 146 | // this should succeed as its being called by the admin 147 | vm.prank(initializeFrom); 148 | (success, ) = deployedAddress.call(initializeCall); 149 | assertEq(success, true); 150 | 151 | // cannot reinitialize 152 | vm.prank(initializeFrom); 153 | (success, ) = deployedAddress.call(initializeCall); 154 | assertEq(success, false); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/zora/Zora.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.28; 3 | 4 | /* 5 | 6 | 7 | .. 8 | .:. ..::. .. .. 9 | .==-. .::-=.. ... ..-. .:. 10 | :=++=:====.. .:===-=--:. .:.....::-. 11 | .:-+======.. :-=-=:. ....:..-.. 12 | ::-========-:. .:==: =**#%@@@@%=..-=:---@@@+ .:::::.... 13 | .::-==========+#@@@@@@@@@@@@+ :%@@@@@@@@%: .@@@@@@@@@@@@=: .:@@@@%. .-----: 14 | .....:==+==+#@@@@@@@@@@@@# =@@@@@=-+@@@@@. %@@*.....+@@@. @@@@@@-... .-. 15 | .. .. .-+++-:::----=--#@@@@..@@@@ .%@@@.+@@@. +@@@ *@@-*@@*. .. 16 | .:-. :.. .-++-: ..%@@@% #@@@. .@@@@.@@@= +@@@. +@@# :@@@: 17 | ::..... :-+-: .@@@@# %@@@ :@@@=*@@@@@@@@@@@@ =@@% .%@@+ 18 | .:.:::-::--:. .@@@@* %@@@ .@@@@:@@@@@@@@@%: :@@@..:-%@@% 19 | .:..::--:..-: .-@@@@+ #@@@: %@@@:#@@@ :%@@@*. @@@@@@@@@@@@:. 20 | ......::. .: +@@@@= *@@@# #@@@-=@@@. -@@@@- %@@@@@@@@@@@@#. 21 | :. . .*@@@@- .@@@@- .@@@@- @@@@. .%@@@%. +@@@. *@@@: 22 | .. .%@@@@: *@@@@*. . #@@@@. =@@@: -@@@@==@@@. :@@@+ 23 | .@@@@@. .......*@@@@@@**%@@@@@= :@@@%. %@@@@@@@= .....==%%*= 24 | .@@@@@%%@@@@@@@@@@-.=@@@@@@@@@@@@+. %@@@: =*+=-::. ..-=====:.. 25 | :@@@@@@@@@@@@@@@@@+=--+@@@@@@@@%. .=+=+-. 26 | -@@@@@@@@@@@@@%#*+:---======-::.. :=-..-+: 27 | -=:. ...:=------=====-:::.. .. .:. 28 | .:-========---:....... . 29 | .--======-====-:. 30 | .--------:..--===:: 31 | .::::.... .--==-- 32 | .. .--=-. 33 | .:--: 34 | .::. 35 | .:. 36 | . 37 | 38 | 39 | */ 40 | 41 | import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; 42 | import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; 43 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 44 | import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol"; 45 | import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 46 | import {IZora} from "./IZora.sol"; 47 | 48 | /// @notice Zora Token contract 49 | contract Zora is IZora, ERC20, ERC20Permit, ERC20Votes, Initializable { 50 | /// @notice Account allowed to mint total supply and set the contract URI upon initialization 51 | address immutable initializerAccount; 52 | 53 | /// @notice Contract URI for token metadata 54 | /// @dev Set only during initialization and cannot be changed afterward 55 | string _contractURI; 56 | 57 | /// @notice Does not fully setup token but stores the user to execute setup at a later time. 58 | /// @param _initializerAccount Account to one-time initialize the contract and mint the supply. 59 | constructor(address _initializerAccount) ERC20("Zora", "ZORA") ERC20Permit("Zora") { 60 | require(_initializerAccount != address(0), InitializerCannotBeAddressZero()); 61 | initializerAccount = _initializerAccount; 62 | } 63 | 64 | /** 65 | * @notice Mints all the supply to the given addresses, and sets the contract URI. 66 | * @dev This is not done in the constructor because we want to be able to mine for a deterministic address 67 | * without changing the creation code of the contract, which would be the case if these values were 68 | * hardcoded in the constructor. 69 | * @param tos array of recipient addresses 70 | * @param amounts array of amounts to mint 71 | * @param contractURI_ contract URI to set for the token 72 | */ 73 | function initialize(address[] calldata tos, uint256[] calldata amounts, string memory contractURI_) public initializer { 74 | require(tos.length == amounts.length, InvalidInputLengths()); 75 | require(msg.sender == initializerAccount, OnlyAdmin()); 76 | require(bytes(contractURI_).length > 0, URINeedsToBeSet()); 77 | 78 | _contractURI = contractURI_; 79 | 80 | for (uint256 i = 0; i < tos.length; i++) { 81 | _mint(tos[i], amounts[i]); 82 | } 83 | } 84 | 85 | /// @notice Implements IERC7572 for extended token metadata 86 | /// @return Contract URI set for the token 87 | function contractURI() external view returns (string memory) { 88 | return _contractURI; 89 | } 90 | 91 | /// @dev Needed to override this OZ function to allow for both ERC20 and ERC20Votes inheritance 92 | /// @notice No-op inheritance fix for allowing votes and permit functionality. The shared nonce behaves the same in this case. 93 | /// @param from transfer from address for transfer hook 94 | /// @param to transfer to address for transfer hook 95 | /// @param amount quantity to transfer for hook 96 | function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) { 97 | super._update(from, to, amount); 98 | } 99 | 100 | /// @dev Needed to override this OZ function to allow for both ERC20Permit and general OZ inheritance 101 | /// @notice No-op inheritance fix for allowing votes and permit functionality. The shared nonce behaves the same in this case. 102 | /// @param owner Owner to get the nonces from 103 | function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { 104 | return super.nonces(owner); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/claim/IZoraTokenCommunityClaim.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.28; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /// @title IZoraTokenCommunityClaim 7 | /// @notice ERC20 token distribution contract for Zora community members 8 | /// @dev Implements a gas-efficient token claiming method with the following features: 9 | /// - Pre-allocated claims stored directly in contract storage (vs merkle proofs), allowing for gas-efficient 10 | /// claim process and more claims allowed per block. 11 | /// - Two-phase setup: 1) allocation setting by designated setter 2) admin finalizes with transfer of tokens to claim contract 12 | /// - Multiple claim options: direct claim, and claim with signature. 13 | /// - Supports both EOA and smart contract wallet claiming. 14 | interface IZoraTokenCommunityClaim { 15 | struct AccountClaim { 16 | uint96 allocation; 17 | bool claimed; 18 | } 19 | /// @notice Emitted when allocations are set 20 | /// @param allocations The packed allocation data (address + amount) 21 | event AllocationsSet(bytes32[] allocations); 22 | 23 | /// @notice Emitted when tokens are claimed 24 | /// @param account The account that had the allocation 25 | /// @param claimTo The address that received the tokens 26 | /// @param amount The amount of tokens claimed 27 | event Claimed(address indexed account, address indexed claimTo, uint256 amount); 28 | 29 | /// @notice Emitted when the allocation setup is completed 30 | /// @param totalAllocation The total allocation amount 31 | /// @param claimStart The claim start timestamp 32 | event AllocationSetupCompleted(uint256 totalAllocation, uint256 claimStart); 33 | 34 | /// @notice Error thrown when a account tries to claim without having an allocation 35 | error NoAllocation(); 36 | 37 | /// @notice Error thrown when a account tries to claim more than once 38 | error AlreadyClaimed(); 39 | 40 | /// @notice Error thrown when a signature is invalid 41 | error InvalidSignature(); 42 | 43 | /// @notice Error thrown when a signature has expired 44 | error SignatureExpired(uint256 deadline, uint256 currentTime); 45 | 46 | /// @notice Error thrown when the token is invalid 47 | error InvalidToken(); 48 | 49 | /// @notice Error thrown when a non-admin tries to perform an admin-only action 50 | error OnlyAdmin(); 51 | 52 | /// @notice Error thrown when a non-allocation setter tries to perform an allocation setter-only action 53 | error OnlyAllocationSetter(); 54 | 55 | /// @notice Error thrown when trying to claim before the claim period has started 56 | error ClaimNotOpen(); 57 | 58 | /// @notice Error thrown when trying to set allocations after the claim period has started 59 | error ClaimOpened(); 60 | 61 | /// @notice Error thrown when trying to set allocations after the claim period has started 62 | error AllocationSetupAlreadyCompleted(); 63 | 64 | /// @notice Error thrown when the allocation setup is not completed 65 | error AllocationSetupNotCompleted(); 66 | 67 | /// @notice Error thrown when the claim start timestamp is in the past 68 | error ClaimStartInPast(uint256 claimStart, uint256 currentTime); 69 | 70 | /// @notice Returns the token contract address 71 | /// @return The token contract address 72 | function token() external view returns (IERC20); 73 | 74 | /// @notice Returns the allocation for a account 75 | /// @param account The account address 76 | /// @return The allocation amount 77 | function allocations(address account) external view returns (uint256); 78 | 79 | /// @notice Returns whether a has claimed their allocation 80 | /// @param account The account address 81 | /// @return Whether the account has claimed 82 | function hasClaimed(address account) external view returns (bool); 83 | 84 | /// @notice Returns the account claim for a account 85 | /// @param account The account address 86 | /// @return The account claim 87 | function accountClaim(address account) external view returns (AccountClaim memory); 88 | 89 | /// @notice Claims tokens for the caller 90 | /// @param claimTo The address to send the tokens to 91 | function claim(address claimTo) external; 92 | 93 | /// @notice Claims tokens on behalf of a account with their signature 94 | /// @param account The account who is delegating their claim 95 | /// @param claimTo The address to send the tokens to 96 | /// @param deadline The deadline for the signature to be valid 97 | /// @param signature The signature authorizing the claim 98 | function claimWithSignature(address account, address claimTo, uint256 deadline, bytes calldata signature) external; 99 | 100 | /// @notice Returns the domain separator used for EIP-712 signatures 101 | /// @return The domain separator 102 | function getDomainSeparator() external view returns (bytes32); 103 | 104 | /// @notice Returns the admin address 105 | /// @return The admin address 106 | function admin() external view returns (address); 107 | 108 | /// @notice Returns the claim start timestamp 109 | /// @return The claim start timestamp 110 | function claimStart() external view returns (uint256); 111 | 112 | /// @notice Returns whether claiming is open 113 | /// @return Whether claiming is open 114 | function claimIsOpen() external view returns (bool); 115 | 116 | /// @notice Sets allocations using packed data format with a signature 117 | /// @param packedData Array of packed address+allocation data 118 | /// @param nonce A random nonce to prevent replay attacks 119 | /// @param signature The signature authorizing the allocation setting 120 | function setAllocationsWithSignature(bytes32[] calldata packedData, bytes32 nonce, bytes calldata signature) external; 121 | 122 | /// @notice Sets allocations using packed data format 123 | /// @param packedData Array of packed address+allocation data 124 | function setAllocations(bytes32[] calldata packedData) external; 125 | 126 | /// @notice Updates the claim start timestamp, if the claim has not started yet and the allocation setup is complete. 127 | /// @param claimStart_ The new claim start timestamp 128 | /// @dev Only the admin can update the claim start timestamp. 129 | function updateClaimStart(uint256 claimStart_) external; 130 | 131 | /// @notice Returns the allocation setter address 132 | /// @return The allocation setter address 133 | function allocationSetter() external view returns (address); 134 | 135 | /// @notice Returns the total amount of tokens allocated 136 | /// @return The total allocation amount 137 | function totalAllocated() external view returns (uint256); 138 | 139 | /// @notice Returns whether the allocation setup is complete 140 | /// @return Whether the allocation setup is complete 141 | function allocationSetupComplete() external view returns (bool); 142 | 143 | /// @notice Completes the set allocations phase by transferring the total allocated tokens to the claim contract, 144 | /// and setting the claim start time. 145 | /// @param claimStart_ The timestamp when the claim will be open to the public 146 | /// @param transferBalanceFrom The address to transfer the tokens from 147 | /// @dev Can only be called by the admin 148 | function completeAllocationSetup(uint256 claimStart_, address transferBalanceFrom) external; 149 | } 150 | -------------------------------------------------------------------------------- /src/deployment/DeploymentBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {stdJson} from "forge-std/StdJson.sol"; 6 | import {IImmutableCreate2Factory} from "./IImmutableCreate2Factory.sol"; 7 | import {IZora} from "../zora/IZora.sol"; 8 | 9 | contract DeploymentBase is Script { 10 | using stdJson for string; 11 | 12 | address constant IMMUTABLE_CREATE2_FACTORY_ADDRESS = 0x0000000000FFe8B47B3e2130213B802212439497; 13 | 14 | string constant ZORA_TOKEN_CONTRACT_NAME = "token"; 15 | string constant ZORA_TOKEN_COMMUNITY_CLAIM_BASE_CONTRACT_NAME = "claimBase"; 16 | 17 | // Struct to match the JSON structure 18 | struct InitialMint { 19 | address addr; 20 | string amount; 21 | string name; 22 | } 23 | 24 | struct DeploymentConfig { 25 | address admin; 26 | string contractURI; 27 | InitialMint[] initialMints; 28 | } 29 | 30 | struct DeterministicConfig { 31 | bytes32 salt; 32 | address expectedAddress; 33 | bytes creationCode; 34 | } 35 | 36 | struct ClaimConfig { 37 | address admin; 38 | address allocationSetter; 39 | } 40 | 41 | struct AddressesConfig { 42 | address zoraToken; 43 | address zoraTokenCommunityClaim; 44 | address developmentCommunityClaim; 45 | } 46 | 47 | function deploymentConfigPath() internal pure returns (string memory) { 48 | return "script/config/deploymentConfig.json"; 49 | } 50 | 51 | function deterministicConfigPath(string memory contractName) internal pure returns (string memory) { 52 | return string.concat("script/config/deterministic/", contractName, ".json"); 53 | } 54 | 55 | function claimConfigPath(uint256 chainId) internal pure returns (string memory) { 56 | return string.concat("script/config/claim/", vm.toString(chainId), ".json"); 57 | } 58 | 59 | function addressesConfigPath(uint256 chainId) internal pure returns (string memory) { 60 | return string.concat("addresses/", vm.toString(chainId), ".json"); 61 | } 62 | 63 | function getDeploymentConfig() internal view returns (DeploymentConfig memory) { 64 | string memory path = deploymentConfigPath(); 65 | string memory json = vm.readFile(path); 66 | 67 | // Parse the entire JSON into our struct 68 | bytes memory data = vm.parseJson(json); 69 | return abi.decode(data, (DeploymentConfig)); 70 | } 71 | 72 | function getClaimConfig(uint256 chainId) internal view returns (ClaimConfig memory) { 73 | string memory path = claimConfigPath(chainId); 74 | string memory json = vm.readFile(path); 75 | 76 | bytes memory data = vm.parseJson(json); 77 | return abi.decode(data, (ClaimConfig)); 78 | } 79 | 80 | function saveDeterministicConfig(DeterministicConfig memory deterministicConfig, string memory contractName) internal { 81 | string memory objectKey = "config"; 82 | 83 | vm.serializeBytes32(objectKey, "salt", deterministicConfig.salt); 84 | vm.serializeBytes(objectKey, "creationCode", deterministicConfig.creationCode); 85 | string memory result = vm.serializeAddress(objectKey, "expectedAddress", deterministicConfig.expectedAddress); 86 | 87 | vm.writeJson(result, deterministicConfigPath(contractName)); 88 | } 89 | 90 | /// @notice Return a prefixed key for reading with a ".". 91 | /// @param key key to prefix 92 | /// @return prefixed key 93 | function getKeyPrefix(string memory key) internal pure returns (string memory) { 94 | return string.concat(".", key); 95 | } 96 | 97 | function readAddressOrDefaultToZero(string memory json, string memory key) internal view returns (address addr) { 98 | string memory keyPrefix = getKeyPrefix(key); 99 | 100 | if (vm.keyExists(json, keyPrefix)) { 101 | addr = json.readAddress(keyPrefix); 102 | } else { 103 | addr = address(0); 104 | } 105 | } 106 | 107 | function getAddressesConfig() internal view returns (AddressesConfig memory config) { 108 | string memory json = vm.readFile(addressesConfigPath(block.chainid)); 109 | config.zoraToken = readAddressOrDefaultToZero(json, "ZORA_TOKEN"); 110 | config.zoraTokenCommunityClaim = readAddressOrDefaultToZero(json, "ZORA_TOKEN_COMMUNITY_CLAIM"); 111 | config.developmentCommunityClaim = readAddressOrDefaultToZero(json, "DEVELOPMENT_COMMUNITY_CLAIM"); 112 | } 113 | 114 | function saveAddressesConfig(AddressesConfig memory config) internal { 115 | string memory objectKey = "config"; 116 | 117 | vm.serializeAddress(objectKey, "ZORA_TOKEN", config.zoraToken); 118 | vm.serializeAddress(objectKey, "ZORA_TOKEN_COMMUNITY_CLAIM", config.zoraTokenCommunityClaim); 119 | string memory result = vm.serializeAddress(objectKey, "DEVELOPMENT_COMMUNITY_CLAIM", config.developmentCommunityClaim); 120 | vm.writeJson(result, addressesConfigPath(block.chainid)); 121 | } 122 | 123 | function getDeterministicConfig(string memory contractName) internal view returns (DeterministicConfig memory config) { 124 | string memory path = deterministicConfigPath(contractName); 125 | string memory json = vm.readFile(path); 126 | 127 | config.salt = json.readBytes32(".salt"); 128 | config.creationCode = json.readBytes(".creationCode"); 129 | config.expectedAddress = json.readAddress(".expectedAddress"); 130 | } 131 | 132 | function getImmutableCreate2Factory() internal pure returns (IImmutableCreate2Factory) { 133 | return IImmutableCreate2Factory(IMMUTABLE_CREATE2_FACTORY_ADDRESS); 134 | } 135 | 136 | function parseConfigAmount(string memory amountStr) internal pure returns (uint256) { 137 | bytes memory amountBytes = bytes(amountStr); 138 | bytes memory cleanBytes = new bytes(bytes(amountStr).length); 139 | uint256 j = 0; 140 | // remove underscores: 141 | for (uint256 k = 0; k < amountBytes.length; k++) { 142 | if (amountBytes[k] != "_") { 143 | cleanBytes[j] = amountBytes[k]; 144 | j++; 145 | } 146 | } 147 | // Create new string with exact length 148 | bytes memory finalBytes = new bytes(j); 149 | for (uint256 k = 0; k < j; k++) { 150 | finalBytes[k] = cleanBytes[k]; 151 | } 152 | return vm.parseUint(string(finalBytes)) * 10 ** 18; 153 | } 154 | 155 | function getInitialMints() internal view returns (address[] memory tos, uint256[] memory amounts) { 156 | DeploymentConfig memory config = getDeploymentConfig(); 157 | 158 | tos = new address[](config.initialMints.length); 159 | amounts = new uint256[](config.initialMints.length); 160 | 161 | for (uint256 i = 0; i < config.initialMints.length; i++) { 162 | tos[i] = config.initialMints[i].addr; 163 | amounts[i] = parseConfigAmount(config.initialMints[i].amount); 164 | } 165 | } 166 | 167 | function getInitializeCall() internal view returns (address from, bytes memory call) { 168 | DeploymentConfig memory config = getDeploymentConfig(); 169 | 170 | from = config.admin; 171 | 172 | (address[] memory tos, uint256[] memory amounts) = getInitialMints(); 173 | call = abi.encodeWithSelector(IZora.initialize.selector, tos, amounts, config.contractURI); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/development/DevelopmentCommunityClaim.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {console} from "forge-std/console.sol"; 6 | import {Zora} from "../../src/zora/Zora.sol"; 7 | import {DevelopmentCommunityClaim} from "../../src/development/DevelopmentCommunityClaim.sol"; 8 | import {IZoraTokenCommunityClaim} from "../../src/claim/IZoraTokenCommunityClaim.sol"; 9 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 10 | 11 | contract DevelopmentCommunityClaimTest is Test { 12 | address admin; 13 | address allocationSetter; 14 | uint256 allocationSetterPrivateKey; 15 | address originalTokenHolder; 16 | Zora token; 17 | DevelopmentCommunityClaim claim; 18 | uint256 claimStart; 19 | 20 | function setUp() public { 21 | admin = makeAddr("admin"); 22 | (allocationSetter, allocationSetterPrivateKey) = makeAddrAndKey("allocationSetter"); 23 | originalTokenHolder = makeAddr("originalTokenHolder"); 24 | token = new Zora(admin); 25 | claimStart = block.timestamp + 1 hours; 26 | claim = new DevelopmentCommunityClaim(allocationSetter, admin, claimStart, address(token)); 27 | 28 | // Fund claim contract with tokens 29 | vm.startPrank(admin); 30 | address[] memory claimAddress = new address[](1); 31 | uint256[] memory claimAmount = new uint256[](1); 32 | claimAddress[0] = originalTokenHolder; 33 | claimAmount[0] = 1_000_000_000 * 1e18; 34 | token.initialize(claimAddress, claimAmount, "testing"); 35 | vm.stopPrank(); 36 | 37 | vm.prank(originalTokenHolder); 38 | token.transfer(address(claim), 1_000_000_000 * 1e18); 39 | } 40 | 41 | function toCompactAllocations(address[] memory accounts, uint96[] memory amounts) private pure returns (bytes32[] memory) { 42 | bytes32[] memory packedData = new bytes32[](accounts.length); 43 | for (uint256 i = 0; i < accounts.length; i++) { 44 | // Pack address into lower 160 bits, amount into upper 96 bits 45 | // This matches how the contract unpacks: address from first 160 bits, amount from bits shifted right by 160 46 | packedData[i] = bytes32(uint256(uint160(accounts[i])) | (uint256(amounts[i]) << 160)); 47 | } 48 | return packedData; 49 | } 50 | 51 | function testSetAllocations() public { 52 | address user1 = makeAddr("user1"); 53 | address user2 = makeAddr("user2"); 54 | 55 | address[] memory accounts = new address[](2); 56 | uint96[] memory amounts = new uint96[](2); 57 | accounts[0] = user1; 58 | accounts[1] = user2; 59 | amounts[0] = 100 * 1e18; 60 | amounts[1] = 200 * 1e18; 61 | 62 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 63 | 64 | vm.prank(allocationSetter); 65 | claim.setAllocations(compactAllocations); 66 | 67 | assertEq(claim.allocations(user1), 100 * 1e18); 68 | assertEq(claim.allocations(user2), 200 * 1e18); 69 | } 70 | 71 | function testCannotSetAllocationsIfNotAllocationSetter() public { 72 | address user1 = makeAddr("user1"); 73 | 74 | address[] memory accounts = new address[](1); 75 | uint96[] memory amounts = new uint96[](1); 76 | accounts[0] = user1; 77 | amounts[0] = 100 * 1e18; 78 | 79 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 80 | 81 | vm.prank(makeAddr("not-allocation-setter")); 82 | vm.expectRevert(IZoraTokenCommunityClaim.OnlyAllocationSetter.selector); 83 | claim.setAllocations(compactAllocations); 84 | } 85 | 86 | function testCanSetAllocationsAfterClaimStart() public { 87 | address user1 = makeAddr("user1"); 88 | 89 | address[] memory accounts = new address[](1); 90 | uint96[] memory amounts = new uint96[](1); 91 | accounts[0] = user1; 92 | amounts[0] = 100 * 1e18; 93 | 94 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 95 | 96 | vm.prank(admin); 97 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 98 | 99 | vm.warp(claimStart + 1); 100 | 101 | vm.prank(allocationSetter); 102 | claim.setAllocations(compactAllocations); 103 | } 104 | 105 | function testCanSetAllocationsIfSetupCompleted() public { 106 | address[] memory accounts = new address[](0); 107 | uint96[] memory amounts = new uint96[](0); 108 | 109 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 110 | 111 | vm.prank(allocationSetter); 112 | claim.setAllocations(compactAllocations); 113 | 114 | vm.prank(admin); 115 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 116 | 117 | vm.prank(allocationSetter); 118 | claim.setAllocations(compactAllocations); 119 | } 120 | 121 | function testCannotCompleteSetupIfNotAdmin() public { 122 | vm.prank(makeAddr("not-admin")); 123 | vm.expectRevert(IZoraTokenCommunityClaim.OnlyAdmin.selector); 124 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 125 | } 126 | 127 | function testCanClaimBeforeStart() public { 128 | address user1 = makeAddr("user1"); 129 | 130 | address[] memory accounts = new address[](1); 131 | uint96[] memory amounts = new uint96[](1); 132 | accounts[0] = user1; 133 | amounts[0] = 100 * 1e18; 134 | 135 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 136 | 137 | vm.prank(allocationSetter); 138 | claim.setAllocations(compactAllocations); 139 | 140 | vm.prank(user1); 141 | claim.claim(user1); 142 | } 143 | 144 | function testCanClaimBeforeSetupComplete() public { 145 | address user1 = makeAddr("user1"); 146 | uint96 amount = 100 * 1e18; 147 | 148 | // Set allocation 149 | address[] memory accounts = new address[](1); 150 | uint96[] memory amounts = new uint96[](1); 151 | accounts[0] = user1; 152 | amounts[0] = amount; 153 | 154 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 155 | 156 | vm.prank(allocationSetter); 157 | claim.setAllocations(compactAllocations); 158 | 159 | vm.warp(claimStart + 1); 160 | 161 | vm.prank(user1); 162 | claim.claim(user1); 163 | } 164 | 165 | function _getSetAllocationsDigest(bytes32[] memory packedData, bytes32 nonce) private view returns (bytes32) { 166 | bytes32 structHash = keccak256( 167 | abi.encode(keccak256("SetAllocations(bytes32[] packedData,bytes32 nonce)"), keccak256(abi.encodePacked(packedData)), nonce) 168 | ); 169 | 170 | return keccak256(abi.encodePacked("\x19\x01", claim.getDomainSeparator(), structHash)); 171 | } 172 | 173 | function testCanSetAllocationsWithSignatureAfterSetupComplete() public { 174 | vm.prank(allocationSetter); 175 | 176 | address user1 = makeAddr("user1"); 177 | uint96 amount = 100 * 1e18; 178 | 179 | address[] memory accounts = new address[](1); 180 | uint96[] memory amounts = new uint96[](1); 181 | accounts[0] = user1; 182 | amounts[0] = amount; 183 | 184 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 185 | bytes32 nonce = keccak256("test-nonce"); 186 | 187 | // Create signature 188 | bytes32 digest = _getSetAllocationsDigest(compactAllocations, nonce); 189 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationSetterPrivateKey, digest); 190 | bytes memory signature = abi.encodePacked(r, s, v); 191 | 192 | // Complete setup 193 | vm.prank(admin); 194 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 195 | 196 | // Attempt to set allocations should succeed 197 | claim.setAllocationsWithSignature(compactAllocations, nonce, signature); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/claim/ZoraTokenCommunityClaim.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.28; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; 7 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 8 | import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; 9 | import {IZoraTokenCommunityClaim} from "./IZoraTokenCommunityClaim.sol"; 10 | import {UnorderedNonces} from "../utils/UnorderedNonces.sol"; 11 | 12 | /// @title Zora Token Community Claim 13 | /// @notice ERC20 token distribution contract for Zora community members 14 | /// @dev Implements a gas-efficient token claiming method with the following features: 15 | /// - Pre-allocated claims stored directly in contract storage (vs merkle proofs), allowing for gas-efficient 16 | /// claim process and more claims allowed per block. 17 | /// - Two-phase setup: 1) allocation setting by designated setter 2) admin finalizes with transfer of tokens to claim contract 18 | /// - Multiple claim options: direct claim, and claim with signature. 19 | /// - Supports both EOA and smart contract wallet claiming. 20 | contract ZoraTokenCommunityClaim is EIP712, UnorderedNonces, IZoraTokenCommunityClaim { 21 | // Type hash for the ClaimWithSignature struct 22 | bytes32 private constant CLAIM_TYPEHASH = keccak256("ClaimWithSignature(address user,address claimTo,uint256 deadline)"); 23 | bytes32 private constant SET_ALLOCATIONS_TYPEHASH = keccak256("SetAllocations(bytes32[] packedData,bytes32 nonce)"); 24 | string private constant DOMAIN_NAME = "ZoraTokenCommunityClaim"; 25 | string private constant DOMAIN_VERSION = "1"; 26 | 27 | IERC20 public immutable token; 28 | 29 | mapping(address => AccountClaim) internal accountClaims; 30 | 31 | uint256 public totalAllocated; 32 | bool public allocationSetupComplete; 33 | 34 | address public immutable allocationSetter; 35 | address public immutable admin; 36 | uint256 public claimStart; 37 | 38 | /// @notice Constructor 39 | /// @param _allocationSetter The address of the account that can set allocations. 40 | /// @param _admin The address of the account that can complete the setup. Should be a multisig. 41 | /// @param _token The address of the token to be distributed 42 | constructor(address _allocationSetter, address _admin, address _token) EIP712(DOMAIN_NAME, DOMAIN_VERSION) { 43 | if (_token.code.length == 0) { 44 | revert InvalidToken(); 45 | } 46 | 47 | token = IERC20(_token); 48 | allocationSetter = _allocationSetter; 49 | admin = _admin; 50 | } 51 | 52 | /// @inheritdoc IZoraTokenCommunityClaim 53 | function allocations(address account) public view virtual returns (uint256) { 54 | return uint256(accountClaims[account].allocation); 55 | } 56 | 57 | /// @inheritdoc IZoraTokenCommunityClaim 58 | function hasClaimed(address account) public view virtual returns (bool) { 59 | return accountClaims[account].claimed; 60 | } 61 | 62 | /// @inheritdoc IZoraTokenCommunityClaim 63 | function accountClaim(address account) public view virtual returns (AccountClaim memory) { 64 | return accountClaims[account]; 65 | } 66 | 67 | /// @inheritdoc IZoraTokenCommunityClaim 68 | function setAllocations(bytes32[] calldata packedData) external { 69 | _checkCanSetAllocations(msg.sender); 70 | _setAllocations(packedData); 71 | } 72 | 73 | /// @inheritdoc IZoraTokenCommunityClaim 74 | function setAllocationsWithSignature(bytes32[] calldata packedData, bytes32 nonce, bytes calldata signature) external { 75 | // Verify signature 76 | bytes32 structHash = keccak256(abi.encode(SET_ALLOCATIONS_TYPEHASH, keccak256(abi.encodePacked(packedData)), nonce)); 77 | bytes32 digest = _hashTypedDataV4(structHash); 78 | 79 | // recover the signature, and catch and throw a clean error if its a bad signature 80 | (address recovered, ECDSA.RecoverError error, ) = ECDSA.tryRecover(digest, signature); 81 | require(error == ECDSA.RecoverError.NoError, InvalidSignature()); 82 | 83 | // validate that the recovered address can set allocations and that the allocation setup is not complete 84 | _checkCanSetAllocations(recovered); 85 | 86 | // Use the nonce to prevent replay attacks 87 | _useCheckedNonce(allocationSetter, nonce); 88 | 89 | _setAllocations(packedData); 90 | } 91 | 92 | function _setAllocations(bytes32[] calldata packedData) internal { 93 | uint256 updatedTotalAllocated = totalAllocated; 94 | 95 | for (uint256 i = 0; i < packedData.length; i++) { 96 | // Extract address from first 160 bits 97 | address account = address(uint160(uint256(packedData[i]))); 98 | 99 | // Extract allocation from remaining bits (shift right by 160 bits) 100 | uint96 amount = uint96(uint256(packedData[i]) >> 160); 101 | 102 | // Handle both increase and decrease in allocation safely 103 | uint96 existingAllocation = accountClaims[account].allocation; 104 | if (amount > existingAllocation) { 105 | updatedTotalAllocated += amount - existingAllocation; 106 | } else { 107 | updatedTotalAllocated -= existingAllocation - amount; 108 | } 109 | 110 | // Store the allocation 111 | accountClaims[account].allocation = amount; 112 | } 113 | 114 | totalAllocated = updatedTotalAllocated; 115 | 116 | emit AllocationsSet(packedData); 117 | } 118 | 119 | /// @inheritdoc IZoraTokenCommunityClaim 120 | function claimWithSignature(address account, address claimTo, uint256 deadline, bytes calldata signature) external override { 121 | require(block.timestamp <= deadline, SignatureExpired(deadline, block.timestamp)); 122 | 123 | // Verify signature 124 | // Note: We don't need a nonce for replay protection because: 125 | // 1. Allocations can only be set before claiming starts 126 | // 2. Each address can only claim once (tracked by hasClaimed mapping) 127 | bytes32 structHash = keccak256(abi.encode(CLAIM_TYPEHASH, account, claimTo, deadline)); 128 | 129 | bytes32 digest = _hashTypedDataV4(structHash); 130 | 131 | require(SignatureChecker.isValidSignatureNow(account, digest, signature), InvalidSignature()); 132 | 133 | _claim(account, claimTo); 134 | } 135 | 136 | /// @inheritdoc IZoraTokenCommunityClaim 137 | function completeAllocationSetup(uint256 claimStart_, address transferBalanceFrom) external { 138 | require(msg.sender == admin, OnlyAdmin()); 139 | require(!allocationSetupComplete, AllocationSetupAlreadyCompleted()); 140 | require(claimStart_ > block.timestamp, ClaimStartInPast(claimStart_, block.timestamp)); 141 | 142 | allocationSetupComplete = true; 143 | claimStart = claimStart_; 144 | 145 | emit AllocationSetupCompleted(totalAllocated, claimStart_); 146 | 147 | SafeERC20.safeTransferFrom(token, transferBalanceFrom, address(this), totalAllocated); 148 | } 149 | 150 | /// @inheritdoc IZoraTokenCommunityClaim 151 | function updateClaimStart(uint256 claimStart_) external { 152 | _checkCanUpdateClaimStart(claimStart_); 153 | claimStart = claimStart_; 154 | } 155 | 156 | function _checkCanUpdateClaimStart(uint256 claimStart_) internal view virtual { 157 | // only the admin can update the claim start 158 | require(msg.sender == admin, OnlyAdmin()); 159 | // claim start can only be updated if allocation setup is complete 160 | require(allocationSetupComplete, AllocationSetupNotCompleted()); 161 | // claim start can only be updated if claim start hasn't elapsed 162 | require(block.timestamp < claimStart, ClaimOpened()); 163 | // claim start can only be updated to a future time 164 | require(claimStart_ > block.timestamp, ClaimStartInPast(claimStart_, block.timestamp)); 165 | } 166 | 167 | /// @inheritdoc IZoraTokenCommunityClaim 168 | function claim(address claimTo) external override { 169 | _claim(msg.sender, claimTo); 170 | } 171 | 172 | function _claim(address account, address claimTo) internal { 173 | _checkCanClaim(); 174 | if (accountClaims[account].allocation == 0) { 175 | revert NoAllocation(); 176 | } 177 | if (accountClaims[account].claimed) { 178 | revert AlreadyClaimed(); 179 | } 180 | 181 | uint256 amount = uint256(accountClaims[account].allocation); 182 | // Mark as claimed before transfer 183 | accountClaims[account].claimed = true; 184 | 185 | emit Claimed(account, claimTo, amount); 186 | SafeERC20.safeTransfer(token, claimTo, amount); 187 | } 188 | 189 | /// @inheritdoc IZoraTokenCommunityClaim 190 | function getDomainSeparator() public view virtual returns (bytes32) { 191 | return _domainSeparatorV4(); 192 | } 193 | 194 | /// @inheritdoc IZoraTokenCommunityClaim 195 | function claimIsOpen() public view returns (bool) { 196 | return allocationSetupComplete && block.timestamp >= claimStart; 197 | } 198 | 199 | function _checkCanSetAllocations(address sender) internal view virtual { 200 | require(!allocationSetupComplete, AllocationSetupAlreadyCompleted()); 201 | require(sender == allocationSetter, OnlyAllocationSetter()); 202 | } 203 | 204 | function _checkCanClaim() internal view virtual { 205 | require(allocationSetupComplete, AllocationSetupNotCompleted()); 206 | require(block.timestamp >= claimStart, ClaimNotOpen()); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /script/config/deterministic/claimBase.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationCode": "0x6101c080604052346101ea57606081612020803803809161002082856101ee565b8339810103126101ea5761003381610225565b61004b604061004460208501610225565b9301610225565b6040516100596040826101ee565b6017815260208101907f5a6f7261546f6b656e436f6d6d756e697479436c61696d0000000000000000008252604051916100946040846101ee565b600183526020830191603160f81b83526100ad81610239565b610120526100ba846103cf565b61014052519020918260e05251902080610100524660a0526040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a0815261012360c0826101ee565b5190206080523060c052803b156101db576001600160a01b031661016052610180526101a052604051611b1890816105088239608051816115bc015260a05181611679015260c0518161158d015260e0518161160b01526101005181611631015261012051816107eb01526101405181610814015261016051818181610137015261076901526101805181818161093101528181610bd101526112d301526101a0518181816101a90152818161055001526106660152f35b63c1ab6dc160e01b5f5260045ffd5b5f80fd5b601f909101601f19168101906001600160401b0382119082101761021157604052565b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036101ea57565b908151602081105f146102b3575090601f815111610273576020815191015160208210610264571790565b5f198260200360031b1b161790565b604460209160405192839163305a27a960e01b83528160048401528051918291826024860152018484015e5f828201840152601f01601f19168101030190fd5b6001600160401b038111610211575f54600181811c911680156103c5575b60208210146103b157601f811161037f575b50602092601f821160011461032057928192935f92610315575b50508160011b915f199060031b1c1916175f5560ff90565b015190505f806102fd565b601f198216935f8052805f20915f5b868110610367575083600195961061034f575b505050811b015f5560ff90565b01515f1960f88460031b161c191690555f8080610342565b9192602060018192868501518155019401920161032f565b5f8052601f60205f20910160051c810190601f830160051c015b8181106103a657506102e3565b5f8155600101610399565b634e487b7160e01b5f52602260045260245ffd5b90607f16906102d1565b908151602081105f146103fa575090601f815111610273576020815191015160208210610264571790565b6001600160401b03811161021157600154600181811c911680156104fd575b60208210146103b157601f81116104ca575b50602092601f821160011461046957928192935f9261045e575b50508160011b915f199060031b1c19161760015560ff90565b015190505f80610445565b601f1982169360015f52805f20915f5b8681106104b2575083600195961061049a575b505050811b0160015560ff90565b01515f1960f88460031b161c191690555f808061048c565b91926020600181928685015181550194019201610479565b60015f52601f60205f20910160051c810190601f830160051c015b8181106104f2575061042b565b5f81556001016104e5565b90607f169061041956fe6080806040526004361015610012575f80fd5b5f3560e01c90816303723c3314610e88575080630e230f9714610e14578063188e59ec14610dd45780631e83409a14610d9457806345f7f24914610d5957806347fbc8ae14610cf857806352a9039c14610c875780636325454714610a6a5780636ffb3a4b146109c057806373b2e80e14610955578063778ff2c2146108e757806384b0196e146107b55780639546a7de14610615578063b76f44f214610507578063d7ff1a5714610248578063ed24911d14610208578063f04d688f146101cd578063f851a4401461015f5763fc0c546a146100ed575f80fd5b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5f80fd5b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b576020600654604051908152f35b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b576020610240611576565b604051908152f35b3461015b5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5761027f610ed7565b610287610efa565b60443560643567ffffffffffffffff811161015b576102aa903690600401610f4e565b908242116104d7576103429061033a60405160208101907f0859e74ae0ac93d5152fb92190e0026c3e88144bf0366096b1b8b41b76012a1b825273ffffffffffffffffffffffffffffffffffffffff89169687604083015273ffffffffffffffffffffffffffffffffffffffff8916606083015260808201526080815261033260a082611008565b5190206114fb565b923691611083565b61034c818361153c565b5060048195929510156104aa5715938461048a575b505082156103a4575b50501561037c5761037a916110f0565b005b7f8baa579f000000000000000000000000000000000000000000000000000000005f5260045ffd5b5f9250906103f4610420849360405192839160208301957f1626ba7e0000000000000000000000000000000000000000000000000000000087526024840152604060448401526064830190610f7c565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611008565b5190855afa61042d611a1a565b8161047c575b81610441575b50838061036a565b905060208180518101031261015b57602001517f1626ba7e000000000000000000000000000000000000000000000000000000001483610439565b905060208151101590610433565b73ffffffffffffffffffffffffffffffffffffffff161492508580610361565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b827ff88f0490000000000000000000000000000000000000000000000000000000005f526004524260245260445ffd5b3461015b5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5760043573ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036105ed5760ff60055416156105c55760065442101561059d5761059842824281116110b9565b600655005b7feef336d9000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f40d87422000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f47556579000000000000000000000000000000000000000000000000000000005f5260045ffd5b3461015b5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5760043561064f610efa565b73ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036105ed576005549160ff831661078d5760017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0061037a946106cc42854281116110b9565b1617600555806006557fb96496bdabe32060d30b1b65fdea7320b7df3cde1e3cde0af4c69828f6690c3b6040600454928151908482526020820152a173ffffffffffffffffffffffffffffffffffffffff604051927f23b872dd000000000000000000000000000000000000000000000000000000006020850152166024830152306044830152606482015260648152610767608482611008565b7f000000000000000000000000000000000000000000000000000000000000000061197f565b7f8eef7f30000000000000000000000000000000000000000000000000000000005f5260045ffd5b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5761088b61080f7f0000000000000000000000000000000000000000000000000000000000000000611739565b6108387f00000000000000000000000000000000000000000000000000000000000000006118af565b60206108996040519261084b8385611008565b5f84525f3681376040519586957f0f00000000000000000000000000000000000000000000000000000000000000875260e08588015260e0870190610f7c565b908582036040870152610f7c565b4660608501523060808501525f60a085015283810360c08501528180845192838152019301915f5b8281106108d057505050500390f35b8351855286955093810193928101926001016108c1565b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461015b5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5773ffffffffffffffffffffffffffffffffffffffff6109a1610ed7565b165f526003602052602060ff60405f205460601c166040519015158152f35b3461015b5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5773ffffffffffffffffffffffffffffffffffffffff610a0c610ed7565b5f6020604051610a1b81610fbf565b8281520152165f5260036020526040805f20815190610a3982610fbf565b5460ff60206bffffffffffffffffffffffff831693848152019160601c161515815282519182525115156020820152f35b3461015b5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5760043567ffffffffffffffff811161015b57610ab9903690600401610f1d565b6024359160443567ffffffffffffffff811161015b57610add903690600401610f4e565b6040519160208301907f07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff861161015b5761033a610b9d9285610b556020610ba3988b60051b808c8737810103017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611008565b51902060405160208101917ff6fa18c653395f95873b007425e9b0472584e1284a765ec805681f16513abf238352604082015289606082015260608152610332608082611008565b9061153c565b5060048110156104aa5761037c57610bba906112b1565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016805f52600260205260405f20845f5260205260ff60405f205416610c57579261037a935f52600260205260405f20905f5260205260405f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008254161790556113b8565b83907f17a63d21000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b3461015b5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5773ffffffffffffffffffffffffffffffffffffffff610cd3610ed7565b165f52600360205260206bffffffffffffffffffffffff60405f205416604051908152f35b3461015b5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5760043567ffffffffffffffff811161015b57610d4a61037a913690600401610f1d565b90610d54336112b1565b6113b8565b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b576020600454604051908152f35b3461015b5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5761037a610dce610ed7565b336110f0565b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b57602060ff600554166040519015158152f35b3461015b5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5773ffffffffffffffffffffffffffffffffffffffff610e60610ed7565b165f52600260205260405f206024355f52602052602060ff60405f2054166040519015158152f35b3461015b575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015b5760209060ff6005541680610ecb575b15158152f35b50600654421015610ec5565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361015b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361015b57565b9181601f8401121561015b5782359167ffffffffffffffff831161015b576020808501948460051b01011161015b57565b9181601f8401121561015b5782359167ffffffffffffffff831161015b576020838186019501011161015b57565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602080948051918291828752018686015e5f8582860101520116010190565b6040810190811067ffffffffffffffff821117610fdb57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610fdb57604052565b67ffffffffffffffff8111610fdb57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b92919261108f82611049565b9161109d6040519384611008565b82948184528183011161015b578281602093845f960137010152565b156110c2575050565b7fb4010549000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b60ff60055416156105c55760065442106112895773ffffffffffffffffffffffffffffffffffffffff16805f5260036020526bffffffffffffffffffffffff60405f2054161561126157805f52600360205260ff60405f205460601c166112395761123791815f52600360205273ffffffffffffffffffffffffffffffffffffffff6bffffffffffffffffffffffff60405f20541691835f52600360205260405f206c010000000000000000000000007fffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffff8254161790551680927ff7a40077ff7a04c7e61f6f26fb13774259ddf1b6bce9ecf26a8276cdd39926836020604051858152a3604051917fa9059cbb0000000000000000000000000000000000000000000000000000000060208401526024830152604482015260448152610767606482611008565b565b7f646cf558000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f5f8a655a000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f6b687806000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff6005541661078d5773ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036112fc57565b7fce2582f5000000000000000000000000000000000000000000000000000000005f5260045ffd5b91908110156113345760051b0190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b906bffffffffffffffffffffffff809116911603906bffffffffffffffffffffffff821161138b57565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b6004545f5b838110611434575060045560405190602082528260208301527f07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff831161015b57816040917fbd18231792a7535fff80d85564ce7d95dbdff6819b40ad815711b21a75e6bb499460051b8091848401378101030190a1565b9073ffffffffffffffffffffffffffffffffffffffff611455838686611324565b351691611463818686611324565b3560a01c835f5260036020526bffffffffffffffffffffffff8060405f205416828181115f146114df579061149791611361565b16830180931161138b57600192935b5f52600360205260405f20907fffffffffffffffffffffffffffffffffffffffff000000000000000000000000825416179055016113bd565b6114e891611361565b16830392831161138b57600192936114a6565b604290611506611576565b90604051917f19010000000000000000000000000000000000000000000000000000000000008352600283015260228201522090565b815191906041830361156c576115659250602082015190606060408401519301515f1a9061169f565b9192909190565b50505f9160029190565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016301480611676575b156115de577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f82527f000000000000000000000000000000000000000000000000000000000000000060408201527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260a0815261167060c082611008565b51902090565b507f000000000000000000000000000000000000000000000000000000000000000046146115b5565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0841161172e579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15611723575f5173ffffffffffffffffffffffffffffffffffffffff81161561171957905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b60ff81146117985760ff811690601f8211611770576040519161175d604084611008565b6020808452838101919036833783525290565b7fb3512b0c000000000000000000000000000000000000000000000000000000005f5260045ffd5b506040515f5f548060011c91600182169182156118a5575b60208410831461187857838552849290811561183b57506001146117de575b6117db92500382611008565b90565b505f80805290917f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5635b81831061181f5750509060206117db928201016117cf565b6020919350806001915483858801015201910190918392611807565b602092506117db9491507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001682840152151560051b8201016117cf565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b92607f16926117b0565b60ff81146118d35760ff811690601f8211611770576040519161175d604084611008565b506040515f6001548060011c9160018216918215611975575b60208410831461187857838552849290811561183b5750600114611916576117db92500382611008565b5060015f90815290917fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf65b8183106119595750509060206117db928201016117cf565b6020919350806001915483858801015201910190918392611941565b92607f16926118ec565b5f8073ffffffffffffffffffffffffffffffffffffffff6119b593169360208151910182865af16119ae611a1a565b9083611a49565b80519081151591826119f6575b50506119cb5750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b819250906020918101031261015b576020015180159081150361015b575f806119c2565b3d15611a44573d90611a2b82611049565b91611a396040519384611008565b82523d5f602084013e565b606090565b90611a865750805115611a5e57805190602001fd5b7f1425ea42000000000000000000000000000000000000000000000000000000005f5260045ffd5b81511580611ad9575b611a97575090565b73ffffffffffffffffffffffffffffffffffffffff907f9996b315000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b50803b15611a8f56fea264697066735822122065c74dcedfcb271545a254ab392a2b3bb85bf06df327a60815f1111f3dd46b3664736f6c634300081c0033000000000000000000000000e847ebff4d1b7ae3691f238c5cd5cf691fd1f413000000000000000000000000078362961fcf3e48cdc850f9cbca335d0b47d9680000000000000000000000001111111111166b7fe7bd91427724b487980afc69", 3 | "expectedAddress": "0x0000000002ba96C69b95E32CAAB8fc38bAB8B3F8", 4 | "salt": "0x000000000000000000000000000000000000000000000000ecd41100529ae767" 5 | } 6 | -------------------------------------------------------------------------------- /script/config/deterministic/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationCode": "0x61018080604052346104765760208161364f8038038091610020828561047a565b83398101031261047657516001600160a01b0381168082036104765760405161004a60408261047a565b6004815260208101635a6f726160e01b81526040519061006b60408361047a565b60048252635a6f726160e01b60208301526040519261008b60408561047a565b60048452635a4f524160e01b6020850152604051936100ab60408661047a565b60018552603160f81b60208601908152845190946001600160401b0382116103795760035490600182811c9216801561046c575b602083101461035b5781601f8493116103fe575b50602090601f8311600114610398575f9261038d575b50508160011b915f199060031b1c1916176003555b8051906001600160401b0382116103795760045490600182811c9216801561036f575b602083101461035b5781601f8493116102ed575b50602090601f8311600114610287575f9261027c575b50508160011b915f199060031b1c1916176004555b6101898161049d565b6101205261019684610624565b61014052519020918260e05251902080610100524660a0526040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a081526101ff60c08261047a565b5190206080523060c0521561026d5761016052604051612ef2908161075d823960805181611d51015260a05181611e0e015260c05181611d22015260e05181611da001526101005181611dc601526101205181610b6101526101405181610b8a01526101605181611a7d0152f35b63597417ff60e01b5f5260045ffd5b015190505f8061016b565b60045f9081528281209350601f198516905b8181106102d557509084600195949392106102bd575b505050811b01600455610180565b01515f1960f88460031b161c191690555f80806102af565b92936020600181928786015181550195019301610299565b60045f529091507f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f840160051c81019160208510610351575b90601f859493920160051c01905b8181106103435750610155565b5f8155849350600101610336565b9091508190610328565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610141565b634e487b7160e01b5f52604160045260245ffd5b015190505f80610109565b60035f9081528281209350601f198516905b8181106103e657509084600195949392106103ce575b505050811b0160035561011e565b01515f1960f88460031b161c191690555f80806103c0565b929360206001819287860151815501950193016103aa565b60035f529091507fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f840160051c81019160208510610462575b90601f859493920160051c01905b81811061045457506100f3565b5f8155849350600101610447565b9091508190610439565b91607f16916100df565b5f80fd5b601f909101601f19168101906001600160401b0382119082101761037957604052565b908151602081105f14610517575090601f8151116104d75760208151910151602082106104c8571790565b5f198260200360031b1b161790565b604460209160405192839163305a27a960e01b83528160048401528051918291826024860152018484015e5f828201840152601f01601f19168101030190fd5b6001600160401b03811161037957600554600181811c9116801561061a575b602082101461035b57601f81116105e7575b50602092601f821160011461058657928192935f9261057b575b50508160011b915f199060031b1c19161760055560ff90565b015190505f80610562565b601f1982169360055f52805f20915f5b8681106105cf57508360019596106105b7575b505050811b0160055560ff90565b01515f1960f88460031b161c191690555f80806105a9565b91926020600181928685015181550194019201610596565b60055f52601f60205f20910160051c810190601f830160051c015b81811061060f5750610548565b5f8155600101610602565b90607f1690610536565b908151602081105f1461064f575090601f8151116104d75760208151910151602082106104c8571790565b6001600160401b03811161037957600654600181811c91168015610752575b602082101461035b57601f811161071f575b50602092601f82116001146106be57928192935f926106b3575b50508160011b915f199060031b1c19161760065560ff90565b015190505f8061069a565b601f1982169360065f52805f20915f5b86811061070757508360019596106106ef575b505050811b0160065560ff90565b01515f1960f88460031b161c191690555f80806106e1565b919260206001819286850151815501940192016106ce565b60065f52601f60205f20910160051c810190601f830160051c015b8181106107475750610680565b5f815560010161073a565b90607f169061066e56fe60806040526004361015610011575f80fd5b5f3560e01c806306fdde03146101a4578063095ea7b31461019f57806318160ddd1461019a57806323b872dd14610195578063313ce567146101905780633644e5151461018b5780633a46b1a8146101865780634bf5d7e914610181578063587cde1e1461017c5780635c19a95c146101775780636fcfff451461017257806370a082311461016d5780637c3d85f9146101685780637ecebe001461016357806384b0196e1461015e5780638e539e8c1461015957806391ddadf41461015457806395d89b411461014f5780639ab24eb01461014a578063a9059cbb14610145578063c3cda52014610140578063d505accf1461013b578063dd62ed3e14610136578063e8a3d485146101315763f1127ed81461012c575f80fd5b61146d565b6113aa565b611311565b611150565b610fc0565b610f58565b610ece565b610e0b565b610dc2565b610c80565b610b2b565b610ac4565b6109f0565b610881565b6107d7565b610791565b610714565b61064d565b610563565b610523565b6104ea565b6103c6565b61038b565b610338565b610200565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602080948051918291828752018686015e5f8582860101520116010190565b9060206101fd9281815201906101a9565b90565b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576040515f60035461023e81611544565b80845290600181169081156102d45750600114610276575b6102728361026681850382610964565b604051918291826101ec565b0390f35b60035f9081527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b939250905b8082106102ba57509091508101602001610266610256565b9192600181602092548385880101520191019092916102a2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b840190910191506102669050610256565b5f80fd5b73ffffffffffffffffffffffffffffffffffffffff81160361031657565b346103165760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576103806004356103768161031a565b60243590336121c9565b602060405160018152f35b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576020600254604051908152f35b346103165760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576004356104018161031a565b60243561040d8161031a565b6044359073ffffffffffffffffffffffffffffffffffffffff83165f52600160205261045a3360405f209073ffffffffffffffffffffffffffffffffffffffff165f5260205260405f2090565b54927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff840361049a575b61048e9350611b84565b60405160018152602090f35b8284106104b6576104b18361048e950333836122be565b610484565b82847ffb8f41b2000000000000000000000000000000000000000000000000000000005f523360045260245260445260645ffd5b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261031657602060405160128152f35b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261031657602061055b611d0b565b604051908152f35b346103165760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165760043561059e8161031a565b60243565ffffffffffff6105b143611e34565b168082101561061e5761027279ffffffffffffffffffffffffffffffffffffffffffffffffffff61060d8473ffffffffffffffffffffffffffffffffffffffff87165f52600960205261060760405f2091611e34565b90611ee4565b604051911681529081906020820190565b907fecd3f81e000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165761068443611e34565b65ffffffffffff8061069543611e34565b169116036106ec576102726040516106ae604082610964565b601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c7400000060208201526040519182916020835260208301906101a9565b7f6ff07140000000000000000000000000000000000000000000000000000000005f5260045ffd5b346103165760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165773ffffffffffffffffffffffffffffffffffffffff6004356107648161031a565b165f526008602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b346103165760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576107d56004356107cf8161031a565b33611f75565b005b346103165760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165773ffffffffffffffffffffffffffffffffffffffff6004356108278161031a565b165f52600960205260405f205463ffffffff81116108515760405163ffffffff9091168152602090f35b7f6dfcc650000000000000000000000000000000000000000000000000000000005f52602060045260245260445ffd5b346103165760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261031657602061055b6004356108c18161031a565b73ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f205490565b9181601f840112156103165782359167ffffffffffffffff8311610316576020808501948460051b01011161031657565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040810190811067ffffffffffffffff82111761095f57604052565b610916565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761095f57604052565b604051906109b4604083610964565b565b67ffffffffffffffff811161095f57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b346103165760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165760043567ffffffffffffffff811161031657610a3f9036906004016108e5565b9060243567ffffffffffffffff811161031657610a609036906004016108e5565b906044359367ffffffffffffffff8511610316573660238601121561031657846004013593610a8e856109b6565b94610a9c6040519687610964565b8086523660248289010111610316576020815f9260246107d59a01838a013787010152611635565b346103165760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165773ffffffffffffffffffffffffffffffffffffffff600435610b148161031a565b165f526007602052602060405f2054604051908152f35b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261031657610c27610b857f00000000000000000000000000000000000000000000000000000000000000006126c1565b610bae7f000000000000000000000000000000000000000000000000000000000000000061273a565b6020604051610bbd8282610964565b5f815281610c35818301947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe083013687376040519788977f0f00000000000000000000000000000000000000000000000000000000000000895260e0858a015260e08901906101a9565b9087820360408901526101a9565b914660608701523060808701525f60a087015285830360c087015251918281520192915f5b828110610c6957505050500390f35b835185528695509381019392810192600101610c5a565b346103165760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165760043565ffffffffffff610cc243611e34565b169081811015610d9457610cd590611e34565b600a54905f829160058411610d3b575b610cf19350600a6124d6565b80610d1f575060205f5b79ffffffffffffffffffffffffffffffffffffffffffffffffffff60405191168152f35b610d2a602091611ea9565b600a5f52815f20015460301c610cfb565b9192610d4681612363565b8103908111610d8f57610cf193600a5f5265ffffffffffff8260205f2001541665ffffffffffff8516105f14610d7d575091610ce5565b929150610d8990611ed6565b90610ce5565b611e7c565b7fecd3f81e000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576020610dfb43611e34565b65ffffffffffff60405191168152f35b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576040515f600454610e4981611544565b80845290600181169081156102d45750600114610e70576102728361026681850382610964565b60045f9081527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b939250905b808210610eb457509091508101602001610266610256565b919260018160209254838588010152019101909291610e9c565b346103165760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165773ffffffffffffffffffffffffffffffffffffffff600435610f1e8161031a565b165f526009602052602079ffffffffffffffffffffffffffffffffffffffffffffffffffff610f4f60405f20612112565b16604051908152f35b346103165760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261031657610380600435610f968161031a565b6024359033611b84565b6064359060ff8216820361031657565b6084359060ff8216820361031657565b346103165760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261031657600435610ffb8161031a565b6024359060443561100a610fa0565b6084359060a43592804211611125579161109d939161108f6110949460405160208101917fe48329057bfd03d55e49b547132e39cffd9c1820ad7b9d4c5307691425d15adf835273ffffffffffffffffffffffffffffffffffffffff8a1660408301528a606083015260808201526080815261108760a082610964565b519020612179565b612771565b90929192612842565b6110ce8173ffffffffffffffffffffffffffffffffffffffff165f52600760205260405f2080549060018201905590565b8093036110df576107d59250611f75565b73ffffffffffffffffffffffffffffffffffffffff91507f752d88c0000000000000000000000000000000000000000000000000000000005f521660045260245260445ffd5b7f4683af0e000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346103165760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165760043561118b8161031a565b6024356111978161031a565b60443590606435926111a7610fb0565b60a43560c435908642116112e557611277926112726111ed8673ffffffffffffffffffffffffffffffffffffffff165f52600760205260405f2080549060018201905590565b9860405160208101917f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9835273ffffffffffffffffffffffffffffffffffffffff89169b8c604084015273ffffffffffffffffffffffffffffffffffffffff8b1660608401528b608084015260a083015260c082015260c0815261108760e082610964565b6121ba565b9373ffffffffffffffffffffffffffffffffffffffff85160361129e576107d593506121c9565b7f4b800e46000000000000000000000000000000000000000000000000000000005f5273ffffffffffffffffffffffffffffffffffffffff8085166004521660245260445ffd5b867f62791302000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346103165760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103165760206113a16004356113518161031a565b73ffffffffffffffffffffffffffffffffffffffff602435916113738361031a565b165f526001835260405f209073ffffffffffffffffffffffffffffffffffffffff165f5260205260405f2090565b54604051908152f35b34610316575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576040515f600b546113e881611544565b80845290600181169081156102d4575060011461140f576102728361026681850382610964565b600b5f9081527f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9939250905b80821061145357509091508101602001610266610256565b91926001816020925483858801015201910190929161143b565b346103165760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610316576004356114a88161031a565b6024359063ffffffff82168203610316576115089173ffffffffffffffffffffffffffffffffffffffff611502926114de611b6c565b506114e7611b6c565b50165f52600960205260405f206114fc611b6c565b50612909565b5061291e565b60408051825165ffffffffffff16815260209283015179ffffffffffffffffffffffffffffffffffffffffffffffffffff169281019290925290f35b90600182811c9216801561158b575b602083101461155e57565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691611553565b5f92918154916115a483611544565b80835292600181169081156115f957506001146115c057505050565b5f9081526020812093945091925b8383106115df575060209250010190565b6001816020929493945483858701015201910191906115ce565b905060209495507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091509291921683830152151560051b010190565b929093917ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00549467ffffffffffffffff61168661167860ff8960401c1615151590565b9767ffffffffffffffff1690565b1680159081611879575b600114908161186f575b159081611866575b5061183e57611723948661171a60017fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000007ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005416177ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0055565b6117c357611a5b565b61172957565b6117947fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054167ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0055565b604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a1565b611839680100000000000000007fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005416177ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0055565b611a5b565b7ff92ee8a9000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050155f6116a2565b303b15915061169a565b879150611690565b601f821161188e57505050565b5f5260205f20906020601f840160051c830193106118c6575b601f0160051c01905b8181106118bb575050565b5f81556001016118b0565b90915081906118a7565b90815167ffffffffffffffff811161095f576118f8816118f1600b54611544565b600b611881565b602092601f821160011461195657611946929382915f9261194b575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b600b55565b015190505f80611914565b600b5f527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08216937f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9915f5b8681106119f757508360019596106119c0575b505050811b01600b55565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690555f80806119b5565b919260206001819286850151815501940192016119a2565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9190811015611a4c5760051b0190565b611a0f565b356101fd8161031a565b93929192828203611b445773ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303611b1c57805115611af457611ab3906118d0565b5f5b818110611ac3575050505050565b80611aee611adc611ad7600194868a611a3c565b611a51565b611ae7838789611a3c565b3590612025565b01611ab5565b7feaa58b59000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f47556579000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc6ec8756000000000000000000000000000000000000000000000000000000005f5260045ffd5b60405190611b7982610943565b5f6020838281520152565b92919073ffffffffffffffffffffffffffffffffffffffff8416938415611cdf5773ffffffffffffffffffffffffffffffffffffffff82168015611cb357611be98273ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f2090565b54848110611c7f5795846109b4969703611c208473ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f2090565b55611c488473ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f2090565b8054860190556040518581527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef90602090a3612be7565b8490877fe450d38c000000000000000000000000000000000000000000000000000000005f5260045260245260445260645ffd5b7fec442f05000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b7f96c6fd1e000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016301480611e0b575b15611d73577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f82527f000000000000000000000000000000000000000000000000000000000000000060408201527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260a08152611e0560c082610964565b51902090565b507f00000000000000000000000000000000000000000000000000000000000000004614611d4a565b65ffffffffffff8111611e4c5765ffffffffffff1690565b7f6dfcc650000000000000000000000000000000000000000000000000000000005f52603060045260245260445ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211610d8f57565b9060018201809211610d8f57565b908154905f829160058411611f22575b611eff9350846124d6565b80611f0a5750505f90565b611f1390611ea9565b905f5260205f20015460301c90565b9192611f2d81612363565b8103908111610d8f57611eff93855f5265ffffffffffff8260205f2001541665ffffffffffff8516105f14611f63575091611ef4565b929150611f6f90611ed6565b90611ef4565b73ffffffffffffffffffffffffffffffffffffffff8181165f81815260086020526040812080548685167fffffffffffffffffffffffff0000000000000000000000000000000000000000821681179092556109b49694169461201f9390928691907f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9080a473ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f205490565b9161253a565b919073ffffffffffffffffffffffffffffffffffffffff83168015611cb357600254828101809111610d8f5760025561207b8473ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f2090565b8054830190556040518281525f907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef90602090a36002549279ffffffffffffffffffffffffffffffffffffffffffffffffffff938481116120e257506109b4929350612b4a565b84907f1cb15d26000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b80548061211f5750505f90565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810111610d8f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff915f5260205f2001015460301c90565b604290612184611d0b565b90604051917f19010000000000000000000000000000000000000000000000000000000000008352600283015260228201522090565b916101fd939161109493612771565b73ffffffffffffffffffffffffffffffffffffffff169081156122925773ffffffffffffffffffffffffffffffffffffffff811692831561226657806122597f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92593855f52600160205260405f209073ffffffffffffffffffffffffffffffffffffffff165f5260205260405f2090565b55604051908152602090a3565b7f94280d62000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b7fe602df05000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b73ffffffffffffffffffffffffffffffffffffffff169081156122925773ffffffffffffffffffffffffffffffffffffffff81161561226657612329915f52600160205260405f209073ffffffffffffffffffffffffffffffffffffffff165f5260205260405f2090565b55565b8115612336570490565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b80156124d1576101fd9061246761246061245661244c61244261243861242e61242460016124125f8b608081901c806124c3575b50806123a66124089260401c90565b806124b6575b506123b78160201c90565b806124a9575b506123c88160101c90565b8061249c575b506123d98160081c90565b8061248f575b506123ea8160041c90565b80612482575b506123fb8160021c90565b80612475575b5060011c90565b61246d5760011c90565b1b61241d818b61232c565b0160011c90565b61241d818a61232c565b61241d818961232c565b61241d818861232c565b61241d818761232c565b61241d818661232c565b61241d818561232c565b809261232c565b90612942565b820160011c90565b600291509201915f612401565b600491509201915f6123f0565b600891509201915f6123df565b601091509201915f6123ce565b602091509201915f6123bd565b604091509201915f6123ac565b608092509050612408612397565b505f90565b91905b8382106124e65750505090565b9091928083169080841860011c8201809211610d8f57845f5265ffffffffffff8260205f2001541665ffffffffffff8416105f146125285750925b91906124d9565b93925061253490611ed6565b91612521565b919073ffffffffffffffffffffffffffffffffffffffff81169273ffffffffffffffffffffffffffffffffffffffff81169084821415806126b8575b612582575b5050505050565b81612635575b505082612597575b808061257b565b61262a6126117fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7249361260b61260579ffffffffffffffffffffffffffffffffffffffffffffffffffff9573ffffffffffffffffffffffffffffffffffffffff165f52600960205260405f2090565b91612954565b90612a28565b6040805192851683529316602082015291829190820190565b0390a25f8080612590565b79ffffffffffffffffffffffffffffffffffffffffffffffffffff6126ae61261161269f7fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7249473ffffffffffffffffffffffffffffffffffffffff165f52600960205260405f2090565b6126a888612954565b906129c4565b0390a25f80612588565b50831515612576565b60ff81146127205760ff811690601f82116126f857604051916126e5604084610964565b6020808452838101919036833783525290565b7fb3512b0c000000000000000000000000000000000000000000000000000000005f5260045ffd5b506040516101fd81612733816005611595565b0382610964565b60ff811461275e5760ff811690601f82116126f857604051916126e5604084610964565b506040516101fd81612733816006611595565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411612800579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa156127f5575f5173ffffffffffffffffffffffffffffffffffffffff8116156127eb57905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b6004111561281557565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b61284b8161280b565b80612854575050565b61285d8161280b565b6001810361288d577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b6128968161280b565b600281036128ca57507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b806128d660039261280b565b146128de5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b8054821015611a4c575f5260205f2001905f90565b9060405161292b81610943565b915465ffffffffffff8116835260301c6020830152565b908082101561294f575090565b905090565b79ffffffffffffffffffffffffffffffffffffffffffffffffffff81116129945779ffffffffffffffffffffffffffffffffffffffffffffffffffff1690565b7f6dfcc650000000000000000000000000000000000000000000000000000000005f5260d060045260245260445ffd5b906129ce43611e34565b9079ffffffffffffffffffffffffffffffffffffffffffffffffffff806129f485612112565b92169116039079ffffffffffffffffffffffffffffffffffffffffffffffffffff8211610d8f57612a2492612d19565b9091565b90612a3243611e34565b9079ffffffffffffffffffffffffffffffffffffffffffffffffffff80612a5885612112565b92169116019079ffffffffffffffffffffffffffffffffffffffffffffffffffff8211610d8f57612a2492612d19565b612a9143611e34565b9079ffffffffffffffffffffffffffffffffffffffffffffffffffff80612ab8600a612112565b921691160179ffffffffffffffffffffffffffffffffffffffffffffffffffff8111610d8f57612a2491600a612d19565b612af243611e34565b9079ffffffffffffffffffffffffffffffffffffffffffffffffffff80612b19600a612112565b921691160379ffffffffffffffffffffffffffffffffffffffffffffffffffff8111610d8f57612a2491600a612d19565b9073ffffffffffffffffffffffffffffffffffffffff6109b492612b75612b7084612954565b612a88565b5050168015612bcf575b60086020527f5eff886ea0ce6ca488a3d6e336d6c0f75f46d19b42c06ce5ee98e42c96d256c7545f91825260409091205473ffffffffffffffffffffffffffffffffffffffff908116911661253a565b612be0612bdb83612954565b612ae9565b5050612b7f565b9073ffffffffffffffffffffffffffffffffffffffff806109b4949316918215612c73575b16908115612c60575b5f52600860205273ffffffffffffffffffffffffffffffffffffffff60405f205416905f52600860205273ffffffffffffffffffffffffffffffffffffffff60405f2054169061253a565b612c6c612bdb84612954565b5050612c15565b612c7f612b7085612954565b5050612c0c565b80546801000000000000000081101561095f57612ca891600182018155612909565b612ced5781516020929092015160301b7fffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000001665ffffffffffff92909216919091179055565b7f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b805492939291908215612e8057612d45612d40612d3585611ea9565b835f5260205f200190565b61291e565b9065ffffffffffff612d5d835165ffffffffffff1690565b8185169182911611612e5857612e09946020948892612d90612d85875165ffffffffffff1690565b65ffffffffffff1690565b03612e0d5750612de892612da6612db192611ea9565b905f5260205f200190565b9065ffffffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000083549260301b169116179055565b015179ffffffffffffffffffffffffffffffffffffffffffffffffffff1690565b9190565b915050612e5391612e2d612e1f6109a5565b65ffffffffffff9093168352565b79ffffffffffffffffffffffffffffffffffffffffffffffffffff881682860152612c86565b612de8565b7f2520601d000000000000000000000000000000000000000000000000000000005f5260045ffd5b612eb79250612e90612e1f6109a5565b79ffffffffffffffffffffffffffffffffffffffffffffffffffff85166020830152612c86565b5f919056fea2646970667358221220fb34db9e0a134d7aa961009295c8620ebdbb47aef709d881b02d2ca61558510364736f6c634300081c0033000000000000000000000000078362961fcf3e48cdc850f9cbca335d0b47d968", 3 | "expectedAddress": "0x1111111111166b7FE7bd91427724B487980aFc69", 4 | "salt": "0x0000000000000000000000000000000000000000000000002b56d300ad18fd56" 5 | } 6 | -------------------------------------------------------------------------------- /script/config/deterministicConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationCode": "0x610180806040523461047d576020816136d480380380916100208285610481565b83398101031261047d57516001600160a01b03811680820361047d5760405161004a604082610481565b6004815260208101635a6f726160e01b81526040519061006b604083610481565b60048252635a6f726160e01b60208301526040519261008b604085610481565b60048452635a4f524160e01b6020850152604051936100ab604086610481565b60018552603160f81b60208601908152845190946001600160401b0382116103805760035490600182811c92168015610473575b60208310146103625781601f849311610405575b50602090601f831160011461039f575f92610394575b50508160011b915f199060031b1c1916176003555b8051906001600160401b0382116103805760045490600182811c92168015610376575b60208310146103625781601f8493116102f4575b50602090601f831160011461028e575f92610283575b50508160011b915f199060031b1c1916176004555b610189816104a4565b610120526101968461062b565b61014052519020918260e05251902080610100524660a0526040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a081526101ff60c082610481565b5190206080523060c052156102745761016052604051612f709081610764823960805181611dcf015260a05181611e8c015260c05181611da0015260e05181611e1e01526101005181611e4401526101205181610b7101526101405181610b9a01526101605181818161159e0152611afb0152f35b63597417ff60e01b5f5260045ffd5b015190505f8061016b565b60045f9081528281209350601f198516905b8181106102dc57509084600195949392106102c4575b505050811b01600455610180565b01515f1960f88460031b161c191690555f80806102b6565b929360206001819287860151815501950193016102a0565b60045f529091507f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f840160051c81019160208510610358575b90601f859493920160051c01905b81811061034a5750610155565b5f815584935060010161033d565b909150819061032f565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610141565b634e487b7160e01b5f52604160045260245ffd5b015190505f80610109565b60035f9081528281209350601f198516905b8181106103ed57509084600195949392106103d5575b505050811b0160035561011e565b01515f1960f88460031b161c191690555f80806103c7565b929360206001819287860151815501950193016103b1565b60035f529091507fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f840160051c81019160208510610469575b90601f859493920160051c01905b81811061045b57506100f3565b5f815584935060010161044e565b9091508190610440565b91607f16916100df565b5f80fd5b601f909101601f19168101906001600160401b0382119082101761038057604052565b908151602081105f1461051e575090601f8151116104de5760208151910151602082106104cf571790565b5f198260200360031b1b161790565b604460209160405192839163305a27a960e01b83528160048401528051918291826024860152018484015e5f828201840152601f01601f19168101030190fd5b6001600160401b03811161038057600554600181811c91168015610621575b602082101461036257601f81116105ee575b50602092601f821160011461058d57928192935f92610582575b50508160011b915f199060031b1c19161760055560ff90565b015190505f80610569565b601f1982169360055f52805f20915f5b8681106105d657508360019596106105be575b505050811b0160055560ff90565b01515f1960f88460031b161c191690555f80806105b0565b9192602060018192868501518155019401920161059d565b60055f52601f60205f20910160051c810190601f830160051c015b818110610616575061054f565b5f8155600101610609565b90607f169061053d565b908151602081105f14610656575090601f8151116104de5760208151910151602082106104cf571790565b6001600160401b03811161038057600654600181811c91168015610759575b602082101461036257601f8111610726575b50602092601f82116001146106c557928192935f926106ba575b50508160011b915f199060031b1c19161760065560ff90565b015190505f806106a1565b601f1982169360065f52805f20915f5b86811061070e57508360019596106106f6575b505050811b0160065560ff90565b01515f1960f88460031b161c191690555f80806106e8565b919260206001819286850151815501940192016106d5565b60065f52601f60205f20910160051c810190601f830160051c015b81811061074e5750610687565b5f8155600101610741565b90607f169061067556fe60806040526004361015610011575f80fd5b5f3560e01c806306fdde03146101b4578063095ea7b3146101af57806318160ddd146101aa57806323b872dd146101a5578063313ce567146101a05780633644e5151461019b5780633a46b1a8146101965780634bf5d7e914610191578063587cde1e1461018c5780635c19a95c146101875780636fcfff451461018257806370a082311461017d5780637c3d85f9146101785780637ecebe001461017357806384b0196e1461016e5780638e539e8c1461016957806391ddadf41461016457806395d89b411461015f5780639ab24eb01461015a578063a9059cbb14610155578063c3cda52014610150578063d505accf1461014b578063dd62ed3e14610146578063e8a3d48514610141578063f1127ed81461013c5763fcfa94ac14610137575f80fd5b611554565b61147d565b6113ba565b611321565b611160565b610fd0565b610f68565b610ede565b610e1b565b610dd2565b610c90565b610b3b565b610ad4565b610a00565b610891565b6107e7565b6107a1565b610724565b61065d565b610573565b610533565b6104fa565b6103d6565b61039b565b610348565b610210565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602080948051918291828752018686015e5f8582860101520116010190565b90602061020d9281815201906101b9565b90565b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576040515f60035461024e816115c2565b80845290600181169081156102e45750600114610286575b6102828361027681850382610974565b604051918291826101fc565b0390f35b60035f9081527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b939250905b8082106102ca57509091508101602001610276610266565b9192600181602092548385880101520191019092916102b2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b840190910191506102769050610266565b5f80fd5b73ffffffffffffffffffffffffffffffffffffffff81160361032657565b346103265760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576103906004356103868161032a565b6024359033612247565b602060405160018152f35b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576020600254604051908152f35b346103265760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576004356104118161032a565b60243561041d8161032a565b6044359073ffffffffffffffffffffffffffffffffffffffff83165f52600160205261046a3360405f209073ffffffffffffffffffffffffffffffffffffffff165f5260205260405f2090565b54927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84036104aa575b61049e9350611c02565b60405160018152602090f35b8284106104c6576104c18361049e9503338361233c565b610494565b82847ffb8f41b2000000000000000000000000000000000000000000000000000000005f523360045260245260445260645ffd5b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261032657602060405160128152f35b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261032657602061056b611d89565b604051908152f35b346103265760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576004356105ae8161032a565b60243565ffffffffffff6105c143611eb2565b168082101561062e5761028279ffffffffffffffffffffffffffffffffffffffffffffffffffff61061d8473ffffffffffffffffffffffffffffffffffffffff87165f52600960205261061760405f2091611eb2565b90611f62565b604051911681529081906020820190565b907fecd3f81e000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265761069443611eb2565b65ffffffffffff806106a543611eb2565b169116036106fc576102826040516106be604082610974565b601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c7400000060208201526040519182916020835260208301906101b9565b7f6ff07140000000000000000000000000000000000000000000000000000000005f5260045ffd5b346103265760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265773ffffffffffffffffffffffffffffffffffffffff6004356107748161032a565b165f526008602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b346103265760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576107e56004356107df8161032a565b33611ff3565b005b346103265760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265773ffffffffffffffffffffffffffffffffffffffff6004356108378161032a565b165f52600960205260405f205463ffffffff81116108615760405163ffffffff9091168152602090f35b7f6dfcc650000000000000000000000000000000000000000000000000000000005f52602060045260245260445ffd5b346103265760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261032657602061056b6004356108d18161032a565b73ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f205490565b9181601f840112156103265782359167ffffffffffffffff8311610326576020808501948460051b01011161032657565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040810190811067ffffffffffffffff82111761096f57604052565b610926565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761096f57604052565b604051906109c4604083610974565b565b67ffffffffffffffff811161096f57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b346103265760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265760043567ffffffffffffffff811161032657610a4f9036906004016108f5565b9060243567ffffffffffffffff811161032657610a709036906004016108f5565b906044359367ffffffffffffffff8511610326573660238601121561032657846004013593610a9e856109c6565b94610aac6040519687610974565b8086523660248289010111610326576020815f9260246107e59a01838a0137870101526116b3565b346103265760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265773ffffffffffffffffffffffffffffffffffffffff600435610b248161032a565b165f526007602052602060405f2054604051908152f35b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261032657610c37610b957f000000000000000000000000000000000000000000000000000000000000000061273f565b610bbe7f00000000000000000000000000000000000000000000000000000000000000006127b8565b6020604051610bcd8282610974565b5f815281610c45818301947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe083013687376040519788977f0f00000000000000000000000000000000000000000000000000000000000000895260e0858a015260e08901906101b9565b9087820360408901526101b9565b914660608701523060808701525f60a087015285830360c087015251918281520192915f5b828110610c7957505050500390f35b835185528695509381019392810192600101610c6a565b346103265760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265760043565ffffffffffff610cd243611eb2565b169081811015610da457610ce590611eb2565b600a54905f829160058411610d4b575b610d019350600a612554565b80610d2f575060205f5b79ffffffffffffffffffffffffffffffffffffffffffffffffffff60405191168152f35b610d3a602091611f27565b600a5f52815f20015460301c610d0b565b9192610d56816123e1565b8103908111610d9f57610d0193600a5f5265ffffffffffff8260205f2001541665ffffffffffff8516105f14610d8d575091610cf5565b929150610d9990611f54565b90610cf5565b611efa565b7fecd3f81e000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576020610e0b43611eb2565b65ffffffffffff60405191168152f35b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576040515f600454610e59816115c2565b80845290600181169081156102e45750600114610e80576102828361027681850382610974565b60045f9081527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b939250905b808210610ec457509091508101602001610276610266565b919260018160209254838588010152019101909291610eac565b346103265760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265773ffffffffffffffffffffffffffffffffffffffff600435610f2e8161032a565b165f526009602052602079ffffffffffffffffffffffffffffffffffffffffffffffffffff610f5f60405f20612190565b16604051908152f35b346103265760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261032657610390600435610fa68161032a565b6024359033611c02565b6064359060ff8216820361032657565b6084359060ff8216820361032657565b346103265760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265760043561100b8161032a565b6024359060443561101a610fb0565b6084359060a4359280421161113557916110ad939161109f6110a49460405160208101917fe48329057bfd03d55e49b547132e39cffd9c1820ad7b9d4c5307691425d15adf835273ffffffffffffffffffffffffffffffffffffffff8a1660408301528a606083015260808201526080815261109760a082610974565b5190206121f7565b6127ef565b909291926128c0565b6110de8173ffffffffffffffffffffffffffffffffffffffff165f52600760205260405f2080549060018201905590565b8093036110ef576107e59250611ff3565b73ffffffffffffffffffffffffffffffffffffffff91507f752d88c0000000000000000000000000000000000000000000000000000000005f521660045260245260445ffd5b7f4683af0e000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346103265760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265760043561119b8161032a565b6024356111a78161032a565b60443590606435926111b7610fc0565b60a43560c435908642116112f557611287926112826111fd8673ffffffffffffffffffffffffffffffffffffffff165f52600760205260405f2080549060018201905590565b9860405160208101917f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9835273ffffffffffffffffffffffffffffffffffffffff89169b8c604084015273ffffffffffffffffffffffffffffffffffffffff8b1660608401528b608084015260a083015260c082015260c0815261109760e082610974565b612238565b9373ffffffffffffffffffffffffffffffffffffffff8516036112ae576107e59350612247565b7f4b800e46000000000000000000000000000000000000000000000000000000005f5273ffffffffffffffffffffffffffffffffffffffff8085166004521660245260445ffd5b867f62791302000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346103265760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103265760206113b16004356113618161032a565b73ffffffffffffffffffffffffffffffffffffffff602435916113838361032a565b165f526001835260405f209073ffffffffffffffffffffffffffffffffffffffff165f5260205260405f2090565b54604051908152f35b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576040515f600b546113f8816115c2565b80845290600181169081156102e4575060011461141f576102828361027681850382610974565b600b5f9081527f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9939250905b80821061146357509091508101602001610276610266565b91926001816020925483858801015201910190929161144b565b346103265760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610326576004356114b88161032a565b6024359063ffffffff82168203610326576115189173ffffffffffffffffffffffffffffffffffffffff611512926114ee611bea565b506114f7611bea565b50165f52600960205260405f2061150c611bea565b50612987565b5061299c565b60408051825165ffffffffffff16815260209283015179ffffffffffffffffffffffffffffffffffffffffffffffffffff169281019290925290f35b34610326575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261032657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b90600182811c92168015611609575b60208310146115dc57565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f16916115d1565b5f9291815491611622836115c2565b8083529260018116908115611677575060011461163e57505050565b5f9081526020812093945091925b83831061165d575060209250010190565b60018160209294939454838587010152019101919061164c565b905060209495507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091509291921683830152151560051b010190565b929093917ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00549467ffffffffffffffff6117046116f660ff8960401c1615151590565b9767ffffffffffffffff1690565b16801590816118f7575b60011490816118ed575b1590816118e4575b506118bc576117a1948661179860017fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000007ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005416177ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0055565b61184157611ad9565b6117a757565b6118127fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054167ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0055565b604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a1565b6118b7680100000000000000007fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005416177ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0055565b611ad9565b7ff92ee8a9000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050155f611720565b303b159150611718565b87915061170e565b601f821161190c57505050565b5f5260205f20906020601f840160051c83019310611944575b601f0160051c01905b818110611939575050565b5f815560010161192e565b9091508190611925565b90815167ffffffffffffffff811161096f576119768161196f600b546115c2565b600b6118ff565b602092601f82116001146119d4576119c4929382915f926119c9575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b600b55565b015190505f80611992565b600b5f527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08216937f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9915f5b868110611a755750836001959610611a3e575b505050811b01600b55565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690555f8080611a33565b91926020600181928685015181550194019201611a20565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9190811015611aca5760051b0190565b611a8d565b3561020d8161032a565b93929192828203611bc25773ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303611b9a57805115611b7257611b319061194e565b5f5b818110611b41575050505050565b80611b6c611b5a611b55600194868a611aba565b611acf565b611b65838789611aba565b35906120a3565b01611b33565b7feaa58b59000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f47556579000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc6ec8756000000000000000000000000000000000000000000000000000000005f5260045ffd5b60405190611bf782610953565b5f6020838281520152565b92919073ffffffffffffffffffffffffffffffffffffffff8416938415611d5d5773ffffffffffffffffffffffffffffffffffffffff82168015611d3157611c678273ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f2090565b54848110611cfd5795846109c4969703611c9e8473ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f2090565b55611cc68473ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f2090565b8054860190556040518581527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef90602090a3612c65565b8490877fe450d38c000000000000000000000000000000000000000000000000000000005f5260045260245260445260645ffd5b7fec442f05000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b7f96c6fd1e000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016301480611e89575b15611df1577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f82527f000000000000000000000000000000000000000000000000000000000000000060408201527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260a08152611e8360c082610974565b51902090565b507f00000000000000000000000000000000000000000000000000000000000000004614611dc8565b65ffffffffffff8111611eca5765ffffffffffff1690565b7f6dfcc650000000000000000000000000000000000000000000000000000000005f52603060045260245260445ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211610d9f57565b9060018201809211610d9f57565b908154905f829160058411611fa0575b611f7d935084612554565b80611f885750505f90565b611f9190611f27565b905f5260205f20015460301c90565b9192611fab816123e1565b8103908111610d9f57611f7d93855f5265ffffffffffff8260205f2001541665ffffffffffff8516105f14611fe1575091611f72565b929150611fed90611f54565b90611f72565b73ffffffffffffffffffffffffffffffffffffffff8181165f81815260086020526040812080548685167fffffffffffffffffffffffff0000000000000000000000000000000000000000821681179092556109c49694169461209d9390928691907f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9080a473ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f205490565b916125b8565b919073ffffffffffffffffffffffffffffffffffffffff83168015611d3157600254828101809111610d9f576002556120f98473ffffffffffffffffffffffffffffffffffffffff165f525f60205260405f2090565b8054830190556040518281525f907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef90602090a36002549279ffffffffffffffffffffffffffffffffffffffffffffffffffff9384811161216057506109c4929350612bc8565b84907f1cb15d26000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b80548061219d5750505f90565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810111610d9f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff915f5260205f2001015460301c90565b604290612202611d89565b90604051917f19010000000000000000000000000000000000000000000000000000000000008352600283015260228201522090565b9161020d93916110a4936127ef565b73ffffffffffffffffffffffffffffffffffffffff169081156123105773ffffffffffffffffffffffffffffffffffffffff81169283156122e457806122d77f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92593855f52600160205260405f209073ffffffffffffffffffffffffffffffffffffffff165f5260205260405f2090565b55604051908152602090a3565b7f94280d62000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b7fe602df05000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b73ffffffffffffffffffffffffffffffffffffffff169081156123105773ffffffffffffffffffffffffffffffffffffffff8116156122e4576123a7915f52600160205260405f209073ffffffffffffffffffffffffffffffffffffffff165f5260205260405f2090565b55565b81156123b4570490565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b801561254f5761020d906124e56124de6124d46124ca6124c06124b66124ac6124a260016124905f8b608081901c80612541575b50806124246124869260401c90565b80612534575b506124358160201c90565b80612527575b506124468160101c90565b8061251a575b506124578160081c90565b8061250d575b506124688160041c90565b80612500575b506124798160021c90565b806124f3575b5060011c90565b6124eb5760011c90565b1b61249b818b6123aa565b0160011c90565b61249b818a6123aa565b61249b81896123aa565b61249b81886123aa565b61249b81876123aa565b61249b81866123aa565b61249b81856123aa565b80926123aa565b906129c0565b820160011c90565b600291509201915f61247f565b600491509201915f61246e565b600891509201915f61245d565b601091509201915f61244c565b602091509201915f61243b565b604091509201915f61242a565b608092509050612486612415565b505f90565b91905b8382106125645750505090565b9091928083169080841860011c8201809211610d9f57845f5265ffffffffffff8260205f2001541665ffffffffffff8416105f146125a65750925b9190612557565b9392506125b290611f54565b9161259f565b919073ffffffffffffffffffffffffffffffffffffffff81169273ffffffffffffffffffffffffffffffffffffffff8116908482141580612736575b612600575b5050505050565b816126b3575b505082612615575b80806125f9565b6126a861268f7fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7249361268961268379ffffffffffffffffffffffffffffffffffffffffffffffffffff9573ffffffffffffffffffffffffffffffffffffffff165f52600960205260405f2090565b916129d2565b90612aa6565b6040805192851683529316602082015291829190820190565b0390a25f808061260e565b79ffffffffffffffffffffffffffffffffffffffffffffffffffff61272c61268f61271d7fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7249473ffffffffffffffffffffffffffffffffffffffff165f52600960205260405f2090565b612726886129d2565b90612a42565b0390a25f80612606565b508315156125f4565b60ff811461279e5760ff811690601f82116127765760405191612763604084610974565b6020808452838101919036833783525290565b7fb3512b0c000000000000000000000000000000000000000000000000000000005f5260045ffd5b5060405161020d816127b1816005611613565b0382610974565b60ff81146127dc5760ff811690601f82116127765760405191612763604084610974565b5060405161020d816127b1816006611613565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0841161287e579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15612873575f5173ffffffffffffffffffffffffffffffffffffffff81161561286957905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b6004111561289357565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b6128c981612889565b806128d2575050565b6128db81612889565b6001810361290b577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b61291481612889565b6002810361294857507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b80612954600392612889565b1461295c5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b8054821015611aca575f5260205f2001905f90565b906040516129a981610953565b915465ffffffffffff8116835260301c6020830152565b90808210156129cd575090565b905090565b79ffffffffffffffffffffffffffffffffffffffffffffffffffff8111612a125779ffffffffffffffffffffffffffffffffffffffffffffffffffff1690565b7f6dfcc650000000000000000000000000000000000000000000000000000000005f5260d060045260245260445ffd5b90612a4c43611eb2565b9079ffffffffffffffffffffffffffffffffffffffffffffffffffff80612a7285612190565b92169116039079ffffffffffffffffffffffffffffffffffffffffffffffffffff8211610d9f57612aa292612d97565b9091565b90612ab043611eb2565b9079ffffffffffffffffffffffffffffffffffffffffffffffffffff80612ad685612190565b92169116019079ffffffffffffffffffffffffffffffffffffffffffffffffffff8211610d9f57612aa292612d97565b612b0f43611eb2565b9079ffffffffffffffffffffffffffffffffffffffffffffffffffff80612b36600a612190565b921691160179ffffffffffffffffffffffffffffffffffffffffffffffffffff8111610d9f57612aa291600a612d97565b612b7043611eb2565b9079ffffffffffffffffffffffffffffffffffffffffffffffffffff80612b97600a612190565b921691160379ffffffffffffffffffffffffffffffffffffffffffffffffffff8111610d9f57612aa291600a612d97565b9073ffffffffffffffffffffffffffffffffffffffff6109c492612bf3612bee846129d2565b612b06565b5050168015612c4d575b60086020527f5eff886ea0ce6ca488a3d6e336d6c0f75f46d19b42c06ce5ee98e42c96d256c7545f91825260409091205473ffffffffffffffffffffffffffffffffffffffff90811691166125b8565b612c5e612c59836129d2565b612b67565b5050612bfd565b9073ffffffffffffffffffffffffffffffffffffffff806109c4949316918215612cf1575b16908115612cde575b5f52600860205273ffffffffffffffffffffffffffffffffffffffff60405f205416905f52600860205273ffffffffffffffffffffffffffffffffffffffff60405f205416906125b8565b612cea612c59846129d2565b5050612c93565b612cfd612bee856129d2565b5050612c8a565b80546801000000000000000081101561096f57612d2691600182018155612987565b612d6b5781516020929092015160301b7fffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000001665ffffffffffff92909216919091179055565b7f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b805492939291908215612efe57612dc3612dbe612db385611f27565b835f5260205f200190565b61299c565b9065ffffffffffff612ddb835165ffffffffffff1690565b8185169182911611612ed657612e87946020948892612e0e612e03875165ffffffffffff1690565b65ffffffffffff1690565b03612e8b5750612e6692612e24612e2f92611f27565b905f5260205f200190565b9065ffffffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000083549260301b169116179055565b015179ffffffffffffffffffffffffffffffffffffffffffffffffffff1690565b9190565b915050612ed191612eab612e9d6109b5565b65ffffffffffff9093168352565b79ffffffffffffffffffffffffffffffffffffffffffffffffffff881682860152612d04565b612e66565b7f2520601d000000000000000000000000000000000000000000000000000000005f5260045ffd5b612f359250612f0e612e9d6109b5565b79ffffffffffffffffffffffffffffffffffffffffffffffffffff85166020830152612d04565b5f919056fea264697066735822122079ad9636151f4292e562c25c721b7859140176a7c1475ad4afda001a276d20f864736f6c634300081c0033000000000000000000000000078362961fcf3e48cdc850f9cbca335d0b47d968", 3 | "expectedAddress": "0x1111111107069D46d9655f9868054aa9f3111111", 4 | "salt": "0x0000000000000000000000000000000000000000877ffc007464f55b145d5c82" 5 | } 6 | -------------------------------------------------------------------------------- /test/ZoraTokenCommunityClaim.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {console} from "forge-std/console.sol"; 6 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import {Zora} from "../src/zora/Zora.sol"; 8 | import {ZoraTokenCommunityClaim} from "../src/claim/ZoraTokenCommunityClaim.sol"; 9 | import {IZoraTokenCommunityClaim} from "../src/claim/IZoraTokenCommunityClaim.sol"; 10 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 11 | import {UnorderedNonces} from "../src/utils/UnorderedNonces.sol"; 12 | 13 | interface IMultiOwnable { 14 | function isValidSignature(bytes32 _messageHash, bytes memory _signature) external view returns (bytes4 magicValue); 15 | } 16 | 17 | contract MockSmartWallet is IMultiOwnable { 18 | bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; // bytes4(keccak256("isValidSignature(bytes32,bytes)")) 19 | mapping(address => bool) public isOwner; 20 | 21 | constructor(address _owner) { 22 | isOwner[_owner] = true; 23 | } 24 | 25 | function addOwner(address _owner) external { 26 | isOwner[_owner] = true; 27 | } 28 | 29 | function isValidSignature(bytes32 _messageHash, bytes memory _signature) public view returns (bytes4 magicValue) { 30 | address signatory = ECDSA.recover(_messageHash, _signature); 31 | 32 | if (isOwner[signatory]) { 33 | return MAGIC_VALUE; 34 | } else { 35 | return bytes4(0); 36 | } 37 | } 38 | 39 | receive() external payable {} 40 | } 41 | 42 | contract ZoraTokenCommunityClaimTest is Test { 43 | address admin; 44 | address allocationSetter; 45 | uint256 allocationSetterPrivateKey; 46 | address originalTokenHolder; 47 | Zora token; 48 | ZoraTokenCommunityClaim claim; 49 | uint256 claimStart; 50 | 51 | function setUp() public { 52 | admin = makeAddr("admin"); 53 | (allocationSetter, allocationSetterPrivateKey) = makeAddrAndKey("allocationSetter"); 54 | originalTokenHolder = makeAddr("originalTokenHolder"); 55 | token = new Zora(admin); 56 | claimStart = block.timestamp + 60 hours; 57 | claim = new ZoraTokenCommunityClaim(allocationSetter, admin, address(token)); 58 | 59 | // Fund claim contract with tokens 60 | vm.startPrank(admin); 61 | address[] memory claimAddress = new address[](1); 62 | uint256[] memory claimAmount = new uint256[](1); 63 | claimAddress[0] = originalTokenHolder; 64 | claimAmount[0] = 1_000_000_000 * 1e18; 65 | token.initialize(claimAddress, claimAmount, "testing"); 66 | vm.stopPrank(); 67 | 68 | vm.prank(originalTokenHolder); 69 | token.approve(address(claim), 1_000_000_000 * 1e18); 70 | } 71 | 72 | function toCompactAllocations(address[] memory accounts, uint96[] memory amounts) private pure returns (bytes32[] memory) { 73 | bytes32[] memory packedData = new bytes32[](accounts.length); 74 | for (uint256 i = 0; i < accounts.length; i++) { 75 | // Pack address into lower 160 bits, amount into upper 96 bits 76 | // This matches how the contract unpacks: address from first 160 bits, amount from bits shifted right by 160 77 | packedData[i] = bytes32(uint256(uint160(accounts[i])) | (uint256(amounts[i]) << 160)); 78 | } 79 | return packedData; 80 | } 81 | 82 | function testConstructorRevertsIfTokenIsNotContract() public { 83 | vm.expectRevert(IZoraTokenCommunityClaim.InvalidToken.selector); 84 | address notAContract = makeAddr("not-a-contract"); 85 | new ZoraTokenCommunityClaim(allocationSetter, admin, notAContract); 86 | } 87 | 88 | function testSetAllocations() public { 89 | address user1 = makeAddr("user1"); 90 | address user2 = makeAddr("user2"); 91 | 92 | address[] memory accounts = new address[](2); 93 | uint96[] memory amounts = new uint96[](2); 94 | accounts[0] = user1; 95 | accounts[1] = user2; 96 | amounts[0] = 100 * 1e18; 97 | amounts[1] = 200 * 1e18; 98 | 99 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 100 | 101 | vm.prank(allocationSetter); 102 | claim.setAllocations(compactAllocations); 103 | 104 | assertEq(claim.allocations(user1), 100 * 1e18); 105 | assertEq(claim.allocations(user2), 200 * 1e18); 106 | } 107 | 108 | function testCannotSetAllocationsIfNotAllocationSetter() public { 109 | address user1 = makeAddr("user1"); 110 | 111 | address[] memory accounts = new address[](1); 112 | uint96[] memory amounts = new uint96[](1); 113 | accounts[0] = user1; 114 | amounts[0] = 100 * 1e18; 115 | 116 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 117 | 118 | vm.prank(makeAddr("not-allocation-setter")); 119 | vm.expectRevert(IZoraTokenCommunityClaim.OnlyAllocationSetter.selector); 120 | claim.setAllocations(compactAllocations); 121 | } 122 | 123 | function testCannotSetAllocationsAfterClaimStart() public { 124 | address user1 = makeAddr("user1"); 125 | 126 | address[] memory accounts = new address[](1); 127 | uint96[] memory amounts = new uint96[](1); 128 | accounts[0] = user1; 129 | amounts[0] = 100 * 1e18; 130 | 131 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 132 | 133 | vm.prank(admin); 134 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 135 | 136 | vm.warp(claimStart + 1); 137 | 138 | vm.prank(allocationSetter); 139 | vm.expectRevert(IZoraTokenCommunityClaim.AllocationSetupAlreadyCompleted.selector); 140 | claim.setAllocations(compactAllocations); 141 | } 142 | 143 | function testCannotSetAllocationsIfSetupCompleted() public { 144 | address[] memory accounts = new address[](0); 145 | uint96[] memory amounts = new uint96[](0); 146 | 147 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 148 | 149 | vm.prank(allocationSetter); 150 | claim.setAllocations(compactAllocations); 151 | 152 | vm.prank(admin); 153 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 154 | 155 | vm.assertFalse(claim.claimIsOpen()); 156 | 157 | vm.prank(allocationSetter); 158 | vm.expectRevert(IZoraTokenCommunityClaim.AllocationSetupAlreadyCompleted.selector); 159 | claim.setAllocations(compactAllocations); 160 | } 161 | 162 | function testCannotCompleteSetupIfNotAdmin() public { 163 | vm.prank(makeAddr("not-admin")); 164 | vm.expectRevert(IZoraTokenCommunityClaim.OnlyAdmin.selector); 165 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 166 | } 167 | 168 | function testCannotCompleteSetupIfAlreadyCompleted() public { 169 | vm.prank(admin); 170 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 171 | 172 | vm.prank(admin); 173 | vm.expectRevert(IZoraTokenCommunityClaim.AllocationSetupAlreadyCompleted.selector); 174 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 175 | } 176 | 177 | function testCannotCompleteSetupWithPastClaimStart() public { 178 | // Try to complete setup with past timestamp 179 | vm.startPrank(admin); 180 | vm.expectRevert(abi.encodeWithSelector(IZoraTokenCommunityClaim.ClaimStartInPast.selector, block.timestamp - 1, block.timestamp)); 181 | claim.completeAllocationSetup(block.timestamp - 1, originalTokenHolder); 182 | vm.stopPrank(); 183 | } 184 | 185 | function testTotalAllocationsIsCorrect() public { 186 | address user1 = makeAddr("user1"); 187 | address user2 = makeAddr("user2"); 188 | address user3 = makeAddr("user3"); 189 | 190 | // Initial setup with user1 and user2 191 | address[] memory accounts = new address[](2); 192 | uint96[] memory amounts = new uint96[](2); 193 | 194 | accounts[0] = user1; 195 | amounts[0] = 100 * 1e18; 196 | accounts[1] = user2; 197 | amounts[1] = 400 * 1e18; 198 | 199 | vm.startPrank(allocationSetter); 200 | // First allocation - verify total 201 | claim.setAllocations(toCompactAllocations(accounts, amounts)); 202 | assertEq(claim.totalAllocated(), (100 + 400) * 1e18); 203 | 204 | // Reduce user2's allocation - verify total updates correctly 205 | amounts[1] = 50 * 1e18; 206 | claim.setAllocations(toCompactAllocations(accounts, amounts)); 207 | assertEq(claim.totalAllocated(), (100 + 50) * 1e18); 208 | 209 | // Increase user1's allocation - verify total updates correctly 210 | amounts[0] = 300 * 1e18; 211 | claim.setAllocations(toCompactAllocations(accounts, amounts)); 212 | assertEq(claim.totalAllocated(), (300 + 50) * 1e18); 213 | 214 | // Update user1 to have 10 allocation and add user3 215 | amounts[0] = 10 * 1e18; 216 | accounts[1] = user3; 217 | amounts[1] = 50 * 1e18; 218 | claim.setAllocations(toCompactAllocations(accounts, amounts)); 219 | assertEq(claim.totalAllocated(), (10 + 50 + 50) * 1e18); 220 | 221 | vm.stopPrank(); 222 | 223 | // Complete setup and verify final token balance 224 | vm.prank(admin); 225 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 226 | assertEq(token.balanceOf(address(claim)), (10 + 50 + 50) * 1e18, "Final token balance incorrect"); 227 | } 228 | 229 | function testBasicClaim() public { 230 | address user1 = makeAddr("user1"); 231 | uint96 amount = 100 * 1e18; 232 | 233 | // Allocation not setup yet 234 | assertEq(claim.accountClaim(user1).claimed, false); 235 | assertEq(claim.accountClaim(user1).allocation, 0); 236 | 237 | // Set allocation 238 | address[] memory accounts = new address[](1); 239 | uint96[] memory amounts = new uint96[](1); 240 | accounts[0] = user1; 241 | amounts[0] = amount; 242 | 243 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 244 | 245 | vm.prank(allocationSetter); 246 | claim.setAllocations(compactAllocations); 247 | 248 | // Allocation not claimed yet 249 | assertEq(claim.accountClaim(user1).claimed, false); 250 | assertEq(claim.accountClaim(user1).allocation, amount); 251 | 252 | vm.expectEmit(true, true, true, true); 253 | emit IZoraTokenCommunityClaim.AllocationSetupCompleted(amount, claimStart); 254 | 255 | vm.prank(admin); 256 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 257 | 258 | vm.assertFalse(claim.claimIsOpen()); 259 | 260 | // Warp to claim period 261 | vm.warp(claimStart + 1); 262 | 263 | vm.assertTrue(claim.claimIsOpen()); 264 | 265 | // Claim 266 | vm.prank(user1); 267 | vm.expectEmit(true, true, true, true); 268 | emit IZoraTokenCommunityClaim.Claimed(user1, user1, amount); 269 | vm.expectEmit(true, true, true, true); 270 | emit IERC20.Transfer(address(claim), user1, amount); 271 | claim.claim(user1); 272 | 273 | // Verify claim data 274 | assertEq(token.balanceOf(user1), amount); 275 | assertEq(claim.allocations(user1), amount); 276 | assertEq(claim.hasClaimed(user1), true); 277 | assertEq(claim.accountClaim(user1).claimed, true); 278 | assertEq(claim.accountClaim(user1).allocation, amount); 279 | } 280 | 281 | function testCannotClaimBeforeCompleteOrStart() public { 282 | address user1 = makeAddr("user1"); 283 | 284 | address[] memory accounts = new address[](1); 285 | uint96[] memory amounts = new uint96[](1); 286 | accounts[0] = user1; 287 | amounts[0] = 100 * 1e18; 288 | 289 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 290 | 291 | vm.prank(allocationSetter); 292 | claim.setAllocations(compactAllocations); 293 | 294 | vm.prank(user1); 295 | vm.expectRevert(IZoraTokenCommunityClaim.AllocationSetupNotCompleted.selector); 296 | claim.claim(user1); 297 | 298 | vm.prank(admin); 299 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 300 | 301 | vm.prank(user1); 302 | vm.expectRevert(IZoraTokenCommunityClaim.ClaimNotOpen.selector); 303 | claim.claim(user1); 304 | } 305 | 306 | function testCannotClaimWithNoAllocation() public { 307 | address user1 = makeAddr("user1"); 308 | 309 | vm.prank(admin); 310 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 311 | 312 | vm.warp(claimStart + 1); 313 | 314 | vm.prank(user1); 315 | vm.expectRevert(IZoraTokenCommunityClaim.NoAllocation.selector); 316 | claim.claim(user1); 317 | } 318 | 319 | function _getSignatureDigest(address user, address claimTo, uint256 deadline) private view returns (bytes32) { 320 | bytes32 structHash = keccak256(abi.encode(keccak256("ClaimWithSignature(address user,address claimTo,uint256 deadline)"), user, claimTo, deadline)); 321 | 322 | return keccak256(abi.encodePacked("\x19\x01", claim.getDomainSeparator(), structHash)); 323 | } 324 | 325 | function testSignatureClaim() public { 326 | uint256 privateKey = 0x1234; 327 | address signer = vm.addr(privateKey); 328 | address recipient = makeAddr("recipient"); 329 | uint96 amount = 100 * 1e18; 330 | 331 | // Set allocation 332 | address[] memory accounts = new address[](1); 333 | uint96[] memory amounts = new uint96[](1); 334 | accounts[0] = signer; 335 | amounts[0] = amount; 336 | 337 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 338 | 339 | vm.prank(allocationSetter); 340 | claim.setAllocations(compactAllocations); 341 | 342 | vm.prank(admin); 343 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 344 | 345 | // Warp to claim period 346 | vm.warp(claimStart + 1); 347 | 348 | // Create signature 349 | uint256 deadline = block.timestamp + 1 days; 350 | bytes32 digest = _getSignatureDigest(signer, recipient, deadline); 351 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); 352 | bytes memory signature = abi.encodePacked(r, s, v); 353 | 354 | // Claim with signature 355 | vm.prank(signer); 356 | vm.expectEmit(true, true, true, true); 357 | emit IZoraTokenCommunityClaim.Claimed(signer, recipient, amount); 358 | vm.expectEmit(true, true, true, true); 359 | emit IERC20.Transfer(address(claim), recipient, amount); 360 | claim.claimWithSignature(signer, recipient, deadline, signature); 361 | 362 | // Verify 363 | assertEq(token.balanceOf(recipient), amount); 364 | assertEq(claim.allocations(signer), amount); 365 | assertEq(claim.hasClaimed(signer), true); 366 | } 367 | 368 | function testCannotReuseSignature() public { 369 | uint256 privateKey = 0x1234; 370 | address signer = vm.addr(privateKey); 371 | address recipient = makeAddr("recipient"); 372 | uint96 amount = 100 * 1e18; 373 | 374 | // Set allocation 375 | address[] memory accounts = new address[](1); 376 | uint96[] memory amounts = new uint96[](1); 377 | accounts[0] = signer; 378 | amounts[0] = amount; 379 | 380 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 381 | 382 | vm.prank(allocationSetter); 383 | claim.setAllocations(compactAllocations); 384 | 385 | vm.prank(admin); 386 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 387 | 388 | vm.warp(claimStart + 1); 389 | 390 | uint256 deadline = block.timestamp + 1 days; 391 | bytes32 digest = _getSignatureDigest(signer, recipient, deadline); 392 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); 393 | bytes memory signature = abi.encodePacked(r, s, v); 394 | 395 | // First claim should succeed 396 | claim.claimWithSignature(signer, recipient, deadline, signature); 397 | 398 | // Second claim should fail due to no allocation 399 | vm.expectRevert(IZoraTokenCommunityClaim.AlreadyClaimed.selector); 400 | claim.claimWithSignature(signer, recipient, deadline, signature); 401 | } 402 | 403 | function testCannotClaimWithExpiredSignature() public { 404 | uint256 privateKey = 0x1234; 405 | address signer = vm.addr(privateKey); 406 | address recipient = makeAddr("recipient"); 407 | 408 | // Set allocation 409 | address[] memory accounts = new address[](1); 410 | uint96[] memory amounts = new uint96[](1); 411 | accounts[0] = signer; 412 | amounts[0] = 100 * 1e18; 413 | 414 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 415 | 416 | vm.prank(allocationSetter); 417 | claim.setAllocations(compactAllocations); 418 | 419 | vm.warp(claimStart + 1); 420 | 421 | uint256 deadline = block.timestamp - 1; 422 | bytes32 digest = _getSignatureDigest(signer, recipient, deadline); 423 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); 424 | bytes memory signature = abi.encodePacked(r, s, v); 425 | 426 | vm.expectRevert(abi.encodeWithSelector(IZoraTokenCommunityClaim.SignatureExpired.selector, deadline, block.timestamp)); 427 | claim.claimWithSignature(signer, recipient, deadline, signature); 428 | } 429 | 430 | function testCannotClaimWithInvalidSignature() public { 431 | uint256 wrongPrivateKey = 0x5678; 432 | address signer = makeAddr("signer"); // Different from the key we'll sign with 433 | address recipient = makeAddr("recipient"); 434 | 435 | // Set allocation 436 | address[] memory accounts = new address[](1); 437 | uint96[] memory amounts = new uint96[](1); 438 | accounts[0] = signer; 439 | amounts[0] = 100 * 1e18; 440 | 441 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 442 | 443 | vm.prank(allocationSetter); 444 | claim.setAllocations(compactAllocations); 445 | 446 | vm.warp(claimStart + 1); 447 | 448 | uint256 deadline = block.timestamp + 1 days; 449 | bytes32 digest = _getSignatureDigest(signer, recipient, deadline); 450 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, digest); 451 | bytes memory signature = abi.encodePacked(r, s, v); 452 | 453 | vm.expectRevert(IZoraTokenCommunityClaim.InvalidSignature.selector); 454 | claim.claimWithSignature(signer, recipient, deadline, signature); 455 | } 456 | 457 | function testCannotClaimWithWrongClaimTo() public { 458 | uint256 privateKey = 0x1234; 459 | address signer = vm.addr(privateKey); 460 | address recipient = makeAddr("recipient"); 461 | address wrongRecipient = makeAddr("wrongRecipient"); 462 | uint96 amount = 100 * 1e18; 463 | 464 | // Set allocation 465 | address[] memory accounts = new address[](1); 466 | uint96[] memory amounts = new uint96[](1); 467 | accounts[0] = signer; 468 | amounts[0] = amount; 469 | 470 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 471 | 472 | vm.prank(allocationSetter); 473 | claim.setAllocations(compactAllocations); 474 | 475 | vm.prank(admin); 476 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 477 | 478 | // Warp to claim period 479 | vm.warp(claimStart + 1); 480 | 481 | // Create signature 482 | uint256 deadline = block.timestamp + 1 days; 483 | bytes32 digest = _getSignatureDigest(signer, recipient, deadline); 484 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); 485 | bytes memory signature = abi.encodePacked(r, s, v); 486 | 487 | // Claim with signature 488 | vm.expectRevert(IZoraTokenCommunityClaim.InvalidSignature.selector); 489 | claim.claimWithSignature(signer, wrongRecipient, deadline, signature); 490 | } 491 | 492 | function testSmartWalletClaim() public { 493 | uint256 ownerPrivateKey = 0x1234; 494 | address owner = vm.addr(ownerPrivateKey); 495 | 496 | // Deploy mock smart wallet 497 | MockSmartWallet smartWallet = new MockSmartWallet(owner); 498 | address recipient = makeAddr("recipient"); 499 | uint96 amount = 100 * 1e18; 500 | 501 | // Set allocation for the smart wallet 502 | address[] memory accounts = new address[](1); 503 | uint96[] memory amounts = new uint96[](1); 504 | accounts[0] = address(smartWallet); 505 | amounts[0] = amount; 506 | 507 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 508 | 509 | vm.prank(allocationSetter); 510 | claim.setAllocations(compactAllocations); 511 | 512 | vm.prank(admin); 513 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 514 | 515 | // Warp to claim period 516 | vm.warp(claimStart + 1); 517 | 518 | // Create signature 519 | uint256 deadline = block.timestamp + 1 days; 520 | bytes32 digest = _getSignatureDigest(address(smartWallet), recipient, deadline); 521 | 522 | // Owner signs the message 523 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); 524 | bytes memory signature = abi.encodePacked(r, s, v); 525 | 526 | // Claim with signature 527 | claim.claimWithSignature(address(smartWallet), recipient, deadline, signature); 528 | 529 | // Verify 530 | assertEq(token.balanceOf(recipient), amount); 531 | assertEq(claim.allocations(address(smartWallet)), amount); 532 | assertEq(claim.hasClaimed(address(smartWallet)), true); 533 | } 534 | 535 | function testSmartWalletClaimWithInvalidSigner() public { 536 | uint256 ownerPrivateKey = 0x1234; 537 | address owner = vm.addr(ownerPrivateKey); 538 | uint256 nonOwnerPrivateKey = 0x5678; 539 | 540 | // Deploy mock smart wallet 541 | MockSmartWallet smartWallet = new MockSmartWallet(owner); 542 | address recipient = makeAddr("recipient"); 543 | uint96 amount = 100 * 1e18; 544 | 545 | // Set allocation for the smart wallet 546 | address[] memory accounts = new address[](1); 547 | uint96[] memory amounts = new uint96[](1); 548 | accounts[0] = address(smartWallet); 549 | amounts[0] = amount; 550 | 551 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 552 | 553 | vm.prank(allocationSetter); 554 | claim.setAllocations(compactAllocations); 555 | 556 | // Warp to claim period 557 | vm.warp(claimStart + 1); 558 | 559 | // Create signature with non-owner 560 | uint256 deadline = block.timestamp + 1 days; 561 | bytes32 digest = _getSignatureDigest(address(smartWallet), recipient, deadline); 562 | 563 | // Non-owner signs the message 564 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(nonOwnerPrivateKey, digest); 565 | bytes memory signature = abi.encodePacked(r, s, v); 566 | 567 | // Attempt to claim with invalid signature should fail 568 | vm.expectRevert(IZoraTokenCommunityClaim.InvalidSignature.selector); 569 | claim.claimWithSignature(address(smartWallet), recipient, deadline, signature); 570 | } 571 | 572 | function testFuzz_AllocationChangesPreserveConsistency(uint8 initialAmount, uint8 newAmount, uint8 finalAmount) public { 573 | // Bound the values to reasonable ranges to avoid extreme values 574 | address user = makeAddr("fuzzUser"); 575 | 576 | // First set initial allocation 577 | address[] memory accounts = new address[](1); 578 | uint96[] memory amounts = new uint96[](1); 579 | accounts[0] = user; 580 | amounts[0] = initialAmount; 581 | 582 | // Set initial allocation 583 | vm.prank(allocationSetter); 584 | claim.setAllocations(toCompactAllocations(accounts, amounts)); 585 | 586 | // Verify initial allocation was set correctly 587 | assertEq(claim.allocations(user), initialAmount); 588 | assertEq(claim.totalAllocated(), initialAmount); 589 | 590 | // Now change the allocation to the new amount 591 | amounts[0] = newAmount; 592 | vm.prank(allocationSetter); 593 | claim.setAllocations(toCompactAllocations(accounts, amounts)); 594 | 595 | // Verify allocation was updated correctly 596 | assertEq(claim.allocations(user), newAmount); 597 | 598 | // Verify total allocated is consistent with the user's allocation 599 | assertEq(claim.totalAllocated(), newAmount); 600 | 601 | // Now change the allocation to the final amount 602 | amounts[0] = finalAmount; 603 | vm.prank(allocationSetter); 604 | claim.setAllocations(toCompactAllocations(accounts, amounts)); 605 | 606 | // Verify allocation was updated correctly 607 | assertEq(claim.allocations(user), finalAmount); 608 | 609 | // Verify total allocated is consistent with the user's allocation 610 | assertEq(claim.totalAllocated(), finalAmount); 611 | } 612 | 613 | function _getSetAllocationsDigest(bytes32[] memory packedData, bytes32 nonce) private view returns (bytes32) { 614 | bytes32 structHash = keccak256( 615 | abi.encode(keccak256("SetAllocations(bytes32[] packedData,bytes32 nonce)"), keccak256(abi.encodePacked(packedData)), nonce) 616 | ); 617 | 618 | return keccak256(abi.encodePacked("\x19\x01", claim.getDomainSeparator(), structHash)); 619 | } 620 | 621 | function testSetAllocationsWithSignature() public { 622 | address user1 = makeAddr("user1"); 623 | address user2 = makeAddr("user2"); 624 | 625 | address[] memory accounts = new address[](2); 626 | uint96[] memory amounts = new uint96[](2); 627 | accounts[0] = user1; 628 | accounts[1] = user2; 629 | amounts[0] = 100 * 1e18; 630 | amounts[1] = 200 * 1e18; 631 | 632 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 633 | bytes32 nonce = keccak256("test-nonce"); 634 | 635 | // Create signature 636 | bytes32 digest = _getSetAllocationsDigest(compactAllocations, nonce); 637 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationSetterPrivateKey, digest); 638 | bytes memory signature = abi.encodePacked(r, s, v); 639 | 640 | // Verify nonce was not used 641 | assertFalse(claim.nonceUsed(allocationSetter, nonce)); 642 | 643 | // Set allocations with signature 644 | claim.setAllocationsWithSignature(compactAllocations, nonce, signature); 645 | 646 | // Verify nonce was used 647 | assertTrue(claim.nonceUsed(allocationSetter, nonce)); 648 | 649 | // Verify allocations were set correctly 650 | assertEq(claim.allocations(user1), 100 * 1e18); 651 | assertEq(claim.allocations(user2), 200 * 1e18); 652 | } 653 | 654 | function testCannotReuseNonceForSetAllocations() public { 655 | address user1 = makeAddr("user1"); 656 | uint96 amount = 100 * 1e18; 657 | 658 | address[] memory accounts = new address[](1); 659 | uint96[] memory amounts = new uint96[](1); 660 | accounts[0] = user1; 661 | amounts[0] = amount; 662 | 663 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 664 | bytes32 nonce = keccak256("test-nonce"); 665 | 666 | // Create signature 667 | bytes32 digest = _getSetAllocationsDigest(compactAllocations, nonce); 668 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationSetterPrivateKey, digest); 669 | bytes memory signature = abi.encodePacked(r, s, v); 670 | 671 | // First set should succeed 672 | claim.setAllocationsWithSignature(compactAllocations, nonce, signature); 673 | 674 | // Second set with same nonce should fail 675 | vm.expectRevert(abi.encodeWithSelector(UnorderedNonces.InvalidAccountNonce.selector, allocationSetter, nonce)); 676 | claim.setAllocationsWithSignature(compactAllocations, nonce, signature); 677 | } 678 | 679 | function testCannotSetAllocationsWithWrongSigner() public { 680 | uint256 wrongPrivateKey = 0x5678; 681 | 682 | address user1 = makeAddr("user1"); 683 | uint96 amount = 100 * 1e18; 684 | 685 | address[] memory accounts = new address[](1); 686 | uint96[] memory amounts = new uint96[](1); 687 | accounts[0] = user1; 688 | amounts[0] = amount; 689 | 690 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 691 | bytes32 nonce = keccak256("test-nonce"); 692 | 693 | // Create signature with wrong key 694 | bytes32 digest = _getSetAllocationsDigest(compactAllocations, nonce); 695 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, digest); 696 | bytes memory signature = abi.encodePacked(r, s, v); 697 | 698 | // Attempt to set allocations should fail 699 | vm.expectRevert(IZoraTokenCommunityClaim.OnlyAllocationSetter.selector); 700 | claim.setAllocationsWithSignature(compactAllocations, nonce, signature); 701 | } 702 | 703 | function testCannotSetAllocationsWithBadSignature() public { 704 | address user1 = makeAddr("user1"); 705 | uint96 amount = 100 * 1e18; 706 | 707 | address[] memory accounts = new address[](1); 708 | uint96[] memory amounts = new uint96[](1); 709 | accounts[0] = user1; 710 | amounts[0] = amount; 711 | 712 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 713 | bytes32 nonce = keccak256("test-nonce"); 714 | 715 | // Create signature with wrong key 716 | bytes memory badSignature = abi.encode("bad-signature"); 717 | 718 | // Attempt to set allocations should fail 719 | vm.expectRevert(IZoraTokenCommunityClaim.InvalidSignature.selector); 720 | claim.setAllocationsWithSignature(compactAllocations, nonce, badSignature); 721 | } 722 | 723 | function testCannotSetAllocationsWithSignatureAfterSetupComplete() public { 724 | vm.prank(allocationSetter); 725 | 726 | address user1 = makeAddr("user1"); 727 | uint96 amount = 100 * 1e18; 728 | 729 | address[] memory accounts = new address[](1); 730 | uint96[] memory amounts = new uint96[](1); 731 | accounts[0] = user1; 732 | amounts[0] = amount; 733 | 734 | bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts); 735 | bytes32 nonce = keccak256("test-nonce"); 736 | 737 | // Create signature 738 | bytes32 digest = _getSetAllocationsDigest(compactAllocations, nonce); 739 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationSetterPrivateKey, digest); 740 | bytes memory signature = abi.encodePacked(r, s, v); 741 | 742 | // Complete setup 743 | vm.prank(admin); 744 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 745 | 746 | // Attempt to set allocations should fail 747 | vm.expectRevert(IZoraTokenCommunityClaim.AllocationSetupAlreadyCompleted.selector); 748 | claim.setAllocationsWithSignature(compactAllocations, nonce, signature); 749 | } 750 | 751 | function testOnlyAdminCanUpdateClaimStart() public { 752 | // First complete the allocation setup 753 | vm.prank(admin); 754 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 755 | 756 | // New claim start time (future) 757 | uint256 newClaimStart = block.timestamp + 2 hours; 758 | 759 | // Non-admin tries to update claim start 760 | address nonAdmin = makeAddr("nonAdmin"); 761 | vm.prank(nonAdmin); 762 | vm.expectRevert(IZoraTokenCommunityClaim.OnlyAdmin.selector); 763 | claim.updateClaimStart(newClaimStart); 764 | 765 | // Admin should be able to update claim start 766 | vm.prank(admin); 767 | claim.updateClaimStart(newClaimStart); 768 | 769 | // Verify claim start was updated 770 | assertEq(claim.claimStart(), newClaimStart); 771 | } 772 | 773 | function testCannotUpdateClaimStartBeforeAllocationSetupComplete() public { 774 | // Try to update claim start before allocation setup is complete 775 | uint256 newClaimStart = block.timestamp + 2 hours; 776 | 777 | vm.prank(admin); 778 | vm.expectRevert(IZoraTokenCommunityClaim.AllocationSetupNotCompleted.selector); 779 | claim.updateClaimStart(newClaimStart); 780 | } 781 | 782 | function testFuzz_CannotUpdateClaimStartAfterClaimOpened(int8 timeBeforeClaimStart, int8 timeAhead) public { 783 | // First complete the allocation setup 784 | vm.prank(admin); 785 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 786 | 787 | // Warp to specified time (before or after claim has started) 788 | vm.warp(uint256(int256(claimStart) + timeBeforeClaimStart)); 789 | 790 | uint256 newClaimStart = uint256(int256(block.timestamp) + timeAhead); 791 | 792 | // Two potential failure conditions: 793 | // 1. Claim period has already started (timeBeforeClaimStart >= 0) 794 | // 2. New claim start is in the past or present (timeAhead <= 0) 795 | 796 | vm.prank(admin); 797 | 798 | // The contract checks for claim opened first, then checks if the new time is in the past 799 | // We need to match this order in our test expectations 800 | if (timeBeforeClaimStart >= 0) { 801 | vm.expectRevert(IZoraTokenCommunityClaim.ClaimOpened.selector); 802 | } else if (timeAhead <= 0) { 803 | vm.expectRevert(abi.encodeWithSelector(IZoraTokenCommunityClaim.ClaimStartInPast.selector, newClaimStart, block.timestamp)); 804 | } 805 | 806 | claim.updateClaimStart(newClaimStart); 807 | } 808 | 809 | function testCannotUpdateClaimStartToPast() public { 810 | // First complete the allocation setup 811 | vm.prank(admin); 812 | claim.completeAllocationSetup(claimStart, originalTokenHolder); 813 | 814 | // Try to set a past timestamp (current time minus 1 second) 815 | uint256 pastTimestamp = block.timestamp - 1; 816 | 817 | vm.prank(admin); 818 | vm.expectRevert(abi.encodeWithSelector(IZoraTokenCommunityClaim.ClaimStartInPast.selector, pastTimestamp, block.timestamp)); 819 | claim.updateClaimStart(pastTimestamp); 820 | } 821 | } 822 | --------------------------------------------------------------------------------