diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..38f9149 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +PRIVATE_KEY= +RPC_URL= +FACTORY_OWNER= diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..6ce781d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,23 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, + + extends: [ + 'eslint:recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], + plugins: ['@typescript-eslint', 'simple-import-sort'], + + rules: { + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + }, + + ignorePatterns: ['dist', 'node_modules'], +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..3d021f0 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,103 @@ + +on: [push] + +name: ci + +jobs: + install: + name: Install dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: actions/setup-node@v1 + with: + node-version: 18 + - uses: actions/cache@master + id: yarn-cache + with: + path: | + node_modules + */*/node_modules + key: ${{ runner.os }}-lerna-${{ hashFiles('**/package.json', '**/yarn.lock') }} + - run: yarn install --network-concurrency 1 + if: ${{ steps.yarn-cache.outputs.cache-hit != 'true' }} + + lint-sol: + name: Solidity lint + runs-on: ubuntu-latest + needs: [install] + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: actions/setup-node@v1 + with: + node-version: 18 + - uses: actions/cache@master + id: yarn-cache + with: + path: | + node_modules + */*/node_modules + key: ${{ runner.os }}-lerna-${{ hashFiles('**/package.json', '**/yarn.lock') }} + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run linting + run: yarn lint:sol + + foundry-tests: + name: Foundry tests + runs-on: ubuntu-latest + needs: [install] + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: actions/setup-node@v1 + with: + node-version: 18 + - uses: actions/cache@master + id: yarn-cache + with: + path: | + node_modules + */*/node_modules + key: ${{ runner.os }}-lerna-${{ hashFiles('**/package.json', '**/yarn.lock') }} + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run tests + run: FOUNDRY_FUZZ_RUNS=1024 forge test -vvv + + # coverage: + # name: Coverage + # runs-on: ubuntu-latest + # needs: [install] + # steps: + # - uses: actions/checkout@v3 + # with: + # submodules: recursive + # - uses: actions/setup-node@v1 + # with: + # node-version: 18 + # - uses: actions/cache@master + # id: yarn-cache + # with: + # path: | + # node_modules + # */*/node_modules + # key: ${{ runner.os }}-lerna-${{ hashFiles('**/package.json', '**/yarn.lock') }} + # - run: yarn coverage || true + # - name: Coveralls + # uses: coverallsapp/github-action@master + # with: + # github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..646835d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Forge +out/ +cache/ + +# Ignore .DS_Store files on macOS +.DS_Store + +# Yarn +node_modules/ +yarn-error.log + +# Env vars +.env diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d6fe983 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/murky"] + path = lib/murky + url = https://github.com/dmfxyz/murky diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..5a182ef --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..9050f2a --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + arrowParens: 'avoid', + semi: false, + singleQuote: true, + trailingComma: 'all', +} diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..99e2121 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,8 @@ +{ + "extends": ["solhint:recommended"], + "rules": { + "compiler-version": ["error", "^0.8.17"], + "func-visibility": ["warn", { "ignoreConstructors": true }], + "reason-string": ["warn", { "maxLength": 96 }] + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9f270f1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": true, + "[solidity]": { + "editor.defaultFormatter": "JuanBlanco.solidity" + }, + "solidity.formatter": "forge", + "solidity.compileUsingRemoteVersion": "v0.8.17" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..df07b30 --- /dev/null +++ b/LICENSE @@ -0,0 +1,219 @@ + Copyright (c) 2023-present Horizon Blockchain Games Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + ------------------------------------------------------------------------ + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index dfce5ec..6d469d3 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ -# contracts-library \ No newline at end of file +# Sequence Contracts Library + +This repository provides a set of smart contracts to facilitate the creation and management of contracts deployable on EVM compatible chains, including ERC20, ERC721, and ERC1155 token standards. These contracts are designed for gas efficiency and reuse via proxy deployments. + +## Features + +Base and preset **implementations of common token standards**: + +* ERC-20 +* ERC-721 +* ERC-1155 + +**Common token functionality**, such as the `ERC2981-Controlled` contract which provides a way to handle royalties in NFTs. + +**Proxy** contracts and factories implementing ERC-1967 and with upgradeability. + +## Usage + +1. Clone the repository +2. Install dependencies with `yarn` +3. Compile the contracts with `yarn build` +4. Run tests with `yarn test` + +### Deployment + +Copy `.env.example` to `.env` and set your wallet configuration. + +```sh +cp .env.example .env +``` + +Then run the deployment script. + +```sh +yarn deploy +``` + +## Dependencies + +The contracts in this repository are built with Solidity ^0.8.17 and use 0xSequence, OpenZeppelin and Azuki contracts for standards implementation and additional functionalities such as access control. + +## License + +All contracts in this repository are released under the Apache-2.0 license. diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..c719c2d --- /dev/null +++ b/foundry.toml @@ -0,0 +1,10 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] +solc = "0.8.17" +via_ir = true +optimizer-runs = 20_000 + +[profile.default.fuzz] +runs = 1_024 diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..73d44ec --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 73d44ec7d124e3831bc5f832267889ffb6f9bc3f diff --git a/lib/murky b/lib/murky new file mode 160000 index 0000000..1d9566b --- /dev/null +++ b/lib/murky @@ -0,0 +1 @@ +Subproject commit 1d9566b908b9702c45d354a1caabe8ef5a69938d diff --git a/package.json b/package.json new file mode 100644 index 0000000..33dde30 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "@0xsequence/contracts-library", + "version": "1.0.0", + "description": "Solidity Contract Library for 0xSequence", + "repository": "https://github.com/0xsequence/contract-library.git", + "bugs": { + "url": "https://github.com/0xsequence/contract-library/issues" + }, + "homepage": "https://github.com/0xsequence/contract-library#README.md", + "source": "src/index.ts", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "author": "Horizon Blockchain Games", + "license": "Apache-2.0", + "scripts": { + "build": "ts-node scripts/build.ts", + "deploy": "ts-node scripts/deploy.ts", + "test": "forge test", + "lint:init": "husky install", + "lint:sol": "solhint \"./src/**/*.sol\" \"./tests/**/*.sol\"", + "format:sol": "forge fmt" + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@0xsequence/erc-1155": "^4.0.3", + "@0xsequence/erc20-meta-token": "^4.0.1", + "@openzeppelin/contracts": "^4.8.3", + "erc721a": "^4.2.3", + "erc721a-upgradeable": "^4.2.3" + }, + "lint-staged": { + "**/*.sol": "yarn lint:sol && yarn format:sol" + }, + "devDependencies": { + "@types/node": "^20.1.0", + "dotenv": "^16.1.4", + "ethers": "^5.7.2", + "husky": "^8.0.3", + "keccak256": "^1.0.6", + "lint-staged": "^13.2.2", + "merkletreejs": "^0.2.32", + "solhint": "^3.4.1", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + } +} diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..7777c91 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,9 @@ +@0xsequence/contracts-library/=src/ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ +murky/=lib/murky/src/ +@0xsequence/erc20-meta-token/=node_modules/@0xsequence/erc20-meta-token/ +@0xsequence/erc-1155/=node_modules/@0xsequence/erc-1155/ +erc721a/=node_modules/erc721a/ +erc721a-upgradeable/=node_modules/erc721a-upgradeable/ +@openzeppelin/=node_modules/@openzeppelin/ diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..0b314b9 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,58 @@ +import { exec as execNonPromise } from 'child_process' +import { copyFile, mkdir, readFile, rmdir, writeFile } from 'fs/promises' +import { join } from 'path' +import util from 'util' +import { BUILD_DIR, DEPLOYABLE_CONTRACT_NAMES } from './constants' +const exec = util.promisify(execNonPromise) + +const main = async () => { + // Clean + try { + await rmdir(BUILD_DIR, { recursive: true }) + } catch (err) { + // Dir not found, ignore + } + + // Build with forge + console.log('Building contracts') + await exec('forge build --extra-output-files metadata') + console.log('Contracts built') + + await mkdir(BUILD_DIR, { recursive: true }) + + // Create the compiler input files + for (const solFile of DEPLOYABLE_CONTRACT_NAMES) { + const forgeOutputDir = `out/${solFile}.sol` + const compilerDetails = JSON.parse( + await readFile(join(forgeOutputDir, `${solFile}.metadata.json`), 'utf8'), + ) + + // Replace source urls with file contents + for (const sourceKey of Object.keys(compilerDetails.sources)) { + compilerDetails.sources[sourceKey] = { + content: await readFile(join(sourceKey), 'utf8'), + } + } + + // Write the compiler input file + await writeFile( + join(BUILD_DIR, `${solFile}.input.json`), + JSON.stringify(compilerDetails), + ) + + // Copy the compiler output too + await copyFile( + `${forgeOutputDir}/${solFile}.json`, + `${BUILD_DIR}/${solFile}.json`, + ) + } +} + +main() + .then(() => { + console.log('Done') + }) + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/scripts/constants.ts b/scripts/constants.ts new file mode 100644 index 0000000..2851ef0 --- /dev/null +++ b/scripts/constants.ts @@ -0,0 +1,8 @@ +export const BUILD_DIR = 'build' +export const DEPLOYABLE_CONTRACT_NAMES = [ + 'ERC20TokenMinterFactory', + 'ERC721TokenMinterFactory', + 'ERC721SaleFactory', + 'ERC1155TokenMinterFactory', + 'ERC1155SaleFactory', +] diff --git a/scripts/deploy.ts b/scripts/deploy.ts new file mode 100644 index 0000000..01117c0 --- /dev/null +++ b/scripts/deploy.ts @@ -0,0 +1,133 @@ +import { readFile } from 'fs/promises' +import { join } from 'path' +import { BUILD_DIR, DEPLOYABLE_CONTRACT_NAMES } from './constants' +import { config as dotenvConfig } from 'dotenv' +import { + ContractFactory, + ContractTransaction, + Signer, + Wallet, + ethers, +} from 'ethers' +import { JsonRpcProvider } from '@ethersproject/providers' + +dotenvConfig() + +const { PRIVATE_KEY, RPC_URL, FACTORY_OWNER } = process.env + +const MAX_GAS_LIMIT = 6000000 + +const singletonFactoryFactory = { + address: '0xce0042B868300000d44A59004Da54A005ffdcf9f', + abi: [ + { + constant: false, + inputs: [ + { + internalType: 'bytes', + type: 'bytes', + }, + { + internalType: 'bytes32', + type: 'bytes32', + }, + ], + name: 'deploy', + outputs: [ + { + internalType: 'address payable', + type: 'address', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + ], +} + +const main = async () => { + if (!PRIVATE_KEY || !RPC_URL || !FACTORY_OWNER) { + throw new Error('Environment vars not set') + } + + // Prep deployer wallet + const provider = new JsonRpcProvider(RPC_URL) + const wallet = new Wallet(PRIVATE_KEY, provider) + + // Create deployer factory + const singletonFactory = new ethers.Contract( + singletonFactoryFactory.address, + singletonFactoryFactory.abi, + wallet, + ) + + // Get deployment files from build dir + for (const solFile of DEPLOYABLE_CONTRACT_NAMES) { + console.log(`Deploying ${solFile}`) + + // Create contract for deployment + const compilerOutput = JSON.parse( + await readFile(join(BUILD_DIR, `${solFile}.json`), 'utf8'), + ) + class MyContractFactory extends ContractFactory { + constructor(signer?: Signer) { + super(compilerOutput.abi, compilerOutput.bytecode.object, signer) + } + } + const contract = new MyContractFactory(wallet) + const contractCode = contract.getDeployTransaction(FACTORY_OWNER).data + if (!contractCode) { + throw new Error(`${solFile} did not return contract code`) + } + + // Check if already deployed + const address = ethers.utils.getAddress( + ethers.utils.hexDataSlice( + ethers.utils.keccak256( + ethers.utils.solidityPack( + ['bytes1', 'address', 'bytes32', 'bytes32'], + [ + '0xff', + singletonFactory.address, + ethers.constants.HashZero, + ethers.utils.keccak256(contractCode), + ], + ), + ), + 12, + ), + ) + + if (ethers.utils.arrayify(await provider.getCode(address)).length > 0) { + console.log( + `Skipping ${solFile} because it has been deployed at ${address}`, + ) + continue + } + + const tx: ContractTransaction = await singletonFactory.deploy( + contractCode, + ethers.constants.HashZero, + { + gasLimit: MAX_GAS_LIMIT, + }, + ) + await tx.wait() + + if (ethers.utils.arrayify(await provider.getCode(address)).length === 0) { + throw new Error(`failed to deploy ${solFile}`) + } + + console.log(`Deployed ${solFile} at ${address}`) + } +} + +main() + .then(() => { + console.log('Done') + }) + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/scripts/generateMerkleTree.ts b/scripts/generateMerkleTree.ts new file mode 100644 index 0000000..6b33cf0 --- /dev/null +++ b/scripts/generateMerkleTree.ts @@ -0,0 +1,23 @@ +import { MerkleTree } from 'merkletreejs' +import { utils } from 'ethers' +import keccak256 from 'keccak256' + +const generateTree = (elements: string[]) => { + const hashed = elements.map(e => utils.solidityKeccak256(['uint256'], [e])) + + const merkleTree = new MerkleTree(hashed, keccak256, { + sort: true, + sortPairs: true, + sortLeaves: true, + }) + + return { + merkleTree, + root: merkleTree.getHexRoot(), + } +} + +const generateProof = (tree: MerkleTree, element: string) => + tree.getHexProof(utils.solidityKeccak256(['uint256'], [element])) + +export { generateTree, generateProof } diff --git a/src/proxies/README.md b/src/proxies/README.md new file mode 100644 index 0000000..3272733 --- /dev/null +++ b/src/proxies/README.md @@ -0,0 +1,36 @@ +# Proxies + +This subsection of the repository contains the implementation of proxy contracts. +These proxies delegate calls to an implementation contract, which allows the logic of the contract to be upgraded without changing the address of the contract. + +## Features + +### Transparent Upgradeable Beacon Proxy + +This proxy follows the [Transparent Upgradeable Proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#TransparentUpgradeableProxy) pattern from OpenZeppelin, unless the implementation is unset, then it uses the [Beacon Proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#BeaconProxy) pattern. + +This allows multiple proxies to be upgraded simultaneously, while also allowing individual upgrades to contracts. + +### Sequence Proxy Factory + +A factory for deploying Transparent Upgradeable Beacon Proxies. + +## Usage + +To use the contracts in this section, import the Sequence Proxy Factory contract and use the call the internal functions. For example: + +```solidity +import {SequenceProxyFactory} from "./proxies/SequenceProxyFactory.sol"; + +contract MyContractFactory is SequenceProxyFactory { + + constructor(address factoryOwner) { + MyImplementation impl = new MyImplementation(); + SequenceProxyFactory._initialize(address(impl), factoryOwner); + } + + function deploy(address proxyOwner, bytes32 salt) external returns (address) { + return _createProxy(salt, proxyOwner, ""); + } +} +``` diff --git a/src/proxies/SequenceProxyFactory.sol b/src/proxies/SequenceProxyFactory.sol new file mode 100644 index 0000000..81d7c58 --- /dev/null +++ b/src/proxies/SequenceProxyFactory.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import { + TransparentUpgradeableBeaconProxy, + ITransparentUpgradeableBeaconProxy +} from "./TransparentUpgradeableBeaconProxy.sol"; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +/** + * An proxy factory that deploys upgradeable beacon proxies. + * @dev The factory owner is able to upgrade the beacon implementation. + * @dev Proxy deployers are able to override the beacon reference with their own. + */ +abstract contract SequenceProxyFactory is Ownable { + UpgradeableBeacon public beacon; + + /** + * Initialize a Sequence Proxy Factory. + * @param implementation The initial beacon implementation. + * @param factoryOwner The owner of the factory. + */ + function _initialize(address implementation, address factoryOwner) internal { + beacon = new UpgradeableBeacon(implementation); + Ownable._transferOwnership(factoryOwner); + } + + /** + * Deploys and initializes a new proxy instance. + * @param _salt The deployment salt. + * @param _proxyOwner The owner of the proxy. + * @param _data The initialization data. + * @return proxyAddress The address of the deployed proxy. + */ + function _createProxy(bytes32 _salt, address _proxyOwner, bytes memory _data) internal returns (address proxyAddress) { + bytes32 saltedHash = keccak256(abi.encodePacked(_salt, _proxyOwner, address(beacon), _data)); + bytes memory bytecode = type(TransparentUpgradeableBeaconProxy).creationCode; + + proxyAddress = Create2.deploy(0, saltedHash, bytecode); + ITransparentUpgradeableBeaconProxy(payable(proxyAddress)).initialize(_proxyOwner, address(beacon), _data); + } + + /** + * Computes the address of a proxy instance. + * @param _salt The deployment salt. + * @param _proxyOwner The owner of the proxy. + * @return proxy The expected address of the deployed proxy. + */ + function _computeProxyAddress(bytes32 _salt, address _proxyOwner, bytes memory _data) internal view returns (address) { + bytes32 saltedHash = keccak256(abi.encodePacked(_salt, _proxyOwner, address(beacon), _data)); + bytes32 bytecodeHash = keccak256(type(TransparentUpgradeableBeaconProxy).creationCode); + + return Create2.computeAddress(saltedHash, bytecodeHash); + } + + /** + * Upgrades the beacon implementation. + * @param implementation The new beacon implementation. + */ + function upgradeBeacon(address implementation) public onlyOwner { + beacon.upgradeTo(implementation); + } +} diff --git a/src/proxies/TransparentUpgradeableBeaconProxy.sol b/src/proxies/TransparentUpgradeableBeaconProxy.sol new file mode 100644 index 0000000..741fd88 --- /dev/null +++ b/src/proxies/TransparentUpgradeableBeaconProxy.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {BeaconProxy, Proxy} from "./openzeppelin/BeaconProxy.sol"; +import {TransparentUpgradeableProxy, ERC1967Proxy} from "./openzeppelin/TransparentUpgradeableProxy.sol"; + +interface ITransparentUpgradeableBeaconProxy { + function initialize(address admin, address beacon, bytes memory data) external; +} + +error InvalidInitialization(); + +contract TransparentUpgradeableBeaconProxy is TransparentUpgradeableProxy, BeaconProxy { + /** + * Decode the initialization data from the msg.data and call the initialize function. + */ + function _dispatchInitialize() private returns (bytes memory) { + _requireZeroValue(); + + (address admin, address beacon, bytes memory data) = abi.decode(msg.data[4:], (address, address, bytes)); + initialize(admin, beacon, data); + + return ""; + } + + function initialize(address admin, address beacon, bytes memory data) internal { + if (_admin() != address(0)) { + // Redundant call. This function can only be called when the admin is not set. + revert InvalidInitialization(); + } + _changeAdmin(admin); + _upgradeBeaconToAndCall(beacon, data, false); + } + + /** + * @dev If the admin is not set, the fallback function is used to initialize the proxy. + * @dev If the admin is set, the fallback function is used to delegatecall the implementation. + */ + function _fallback() internal override (TransparentUpgradeableProxy, Proxy) { + if (_getAdmin() == address(0)) { + bytes memory ret; + bytes4 selector = msg.sig; + if (selector == ITransparentUpgradeableBeaconProxy.initialize.selector) { + ret = _dispatchInitialize(); + // solhint-disable-next-line no-inline-assembly + assembly { + return(add(ret, 0x20), mload(ret)) + } + } + // When the admin is not set, the fallback function is used to initialize the proxy. + revert InvalidInitialization(); + } + TransparentUpgradeableProxy._fallback(); + } + + /** + * Returns the current implementation address. + * @dev This is the implementation address set by the admin, or the beacon implementation. + */ + function _implementation() internal view override (ERC1967Proxy, BeaconProxy) returns (address) { + address implementation = ERC1967Proxy._implementation(); + if (implementation != address(0)) { + return implementation; + } + return BeaconProxy._implementation(); + } +} diff --git a/src/proxies/openzeppelin/BeaconProxy.sol b/src/proxies/openzeppelin/BeaconProxy.sol new file mode 100644 index 0000000..d7891a6 --- /dev/null +++ b/src/proxies/openzeppelin/BeaconProxy.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/beacon/BeaconProxy.sol) + +// Note: This implementation is an exact copy with the constructor removed, and pragma and imports updated. + +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; +import "@openzeppelin/contracts/proxy/Proxy.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +/** + * @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}. + * + * The beacon address is stored in storage slot `uint256(keccak256('eip1967.proxy.beacon')) - 1`, so that it doesn't + * conflict with the storage layout of the implementation behind the proxy. + * + * _Available since v3.4._ + */ +contract BeaconProxy is Proxy, ERC1967Upgrade { + /** + * @dev Returns the current beacon address. + */ + function _beacon() internal view virtual returns (address) { + return _getBeacon(); + } + + /** + * @dev Returns the current implementation address of the associated beacon. + */ + function _implementation() internal view virtual override returns (address) { + return IBeacon(_getBeacon()).implementation(); + } + + /** + * @dev Changes the proxy to use a new beacon. Deprecated: see {_upgradeBeaconToAndCall}. + * + * If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. + * + * Requirements: + * + * - `beacon` must be a contract. + * - The implementation returned by `beacon` must be a contract. + */ + function _setBeacon(address beacon, bytes memory data) internal virtual { + _upgradeBeaconToAndCall(beacon, data, false); + } +} diff --git a/src/proxies/openzeppelin/ERC1967Proxy.sol b/src/proxies/openzeppelin/ERC1967Proxy.sol new file mode 100644 index 0000000..0f400d8 --- /dev/null +++ b/src/proxies/openzeppelin/ERC1967Proxy.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol) + +// Note: This implementation is an exact copy with the constructor removed, and pragma and imports updated. + +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/proxy/Proxy.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +/** + * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an + * implementation address that can be changed. This address is stored in storage in the location specified by + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the + * implementation behind the proxy. + */ +contract ERC1967Proxy is Proxy, ERC1967Upgrade { + /** + * @dev Returns the current implementation address. + */ + function _implementation() internal view virtual override returns (address impl) { + return ERC1967Upgrade._getImplementation(); + } +} diff --git a/src/proxies/openzeppelin/TransparentUpgradeableProxy.sol b/src/proxies/openzeppelin/TransparentUpgradeableProxy.sol new file mode 100644 index 0000000..a51f30e --- /dev/null +++ b/src/proxies/openzeppelin/TransparentUpgradeableProxy.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/transparent/TransparentUpgradeableProxy.sol) + +// Note: This implementation is an exact copy with the constructor removed, and pragma and imports updated. + +pragma solidity ^0.8.17; + +import "./ERC1967Proxy.sol"; + +/** + * @dev Interface for {TransparentUpgradeableProxy}. In order to implement transparency, {TransparentUpgradeableProxy} + * does not implement this interface directly, and some of its functions are implemented by an internal dispatch + * mechanism. The compiler is unaware that these functions are implemented by {TransparentUpgradeableProxy} and will not + * include them in the ABI so this interface must be used to interact with it. + */ +interface ITransparentUpgradeableProxy is IERC1967 { + function admin() external view returns (address); + + function implementation() external view returns (address); + + function changeAdmin(address) external; + + function upgradeTo(address) external; + + function upgradeToAndCall(address, bytes memory) external payable; +} + +/** + * @dev This contract implements a proxy that is upgradeable by an admin. + * + * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector + * clashing], which can potentially be used in an attack, this contract uses the + * https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two + * things that go hand in hand: + * + * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if + * that call matches one of the admin functions exposed by the proxy itself. + * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the + * implementation. If the admin tries to call a function on the implementation it will fail with an error that says + * "admin cannot fallback to proxy target". + * + * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing + * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due + * to sudden errors when trying to call a function from the proxy implementation. + * + * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, + * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. + * + * NOTE: The real interface of this proxy is that defined in `ITransparentUpgradeableProxy`. This contract does not + * inherit from that interface, and instead the admin functions are implicitly implemented using a custom dispatch + * mechanism in `_fallback`. Consequently, the compiler will not produce an ABI for this contract. This is necessary to + * fully implement transparency without decoding reverts caused by selector clashes between the proxy and the + * implementation. + * + * WARNING: It is not recommended to extend this contract to add additional external functions. If you do so, the compiler + * will not check that there are no selector conflicts, due to the note above. A selector clash between any new function + * and the functions declared in {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could + * render the admin operations inaccessible, which could prevent upgradeability. Transparency may also be compromised. + */ +contract TransparentUpgradeableProxy is ERC1967Proxy { + + /** + * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. + * + * CAUTION: This modifier is deprecated, as it could cause issues if the modified function has arguments, and the + * implementation provides a function with the same selector. + */ + modifier ifAdmin() { + if (msg.sender == _getAdmin()) { + _; + } else { + _fallback(); + } + } + + /** + * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior + */ + function _fallback() internal virtual override { + if (msg.sender == _getAdmin()) { + bytes memory ret; + bytes4 selector = msg.sig; + if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) { + ret = _dispatchUpgradeTo(); + } else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) { + ret = _dispatchUpgradeToAndCall(); + } else if (selector == ITransparentUpgradeableProxy.changeAdmin.selector) { + ret = _dispatchChangeAdmin(); + } else if (selector == ITransparentUpgradeableProxy.admin.selector) { + ret = _dispatchAdmin(); + } else if (selector == ITransparentUpgradeableProxy.implementation.selector) { + ret = _dispatchImplementation(); + } else { + revert("TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + } + assembly { + return(add(ret, 0x20), mload(ret)) + } + } else { + super._fallback(); + } + } + + /** + * @dev Returns the current admin. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + */ + function _dispatchAdmin() private returns (bytes memory) { + _requireZeroValue(); + + address admin = _getAdmin(); + return abi.encode(admin); + } + + /** + * @dev Returns the current implementation. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` + */ + function _dispatchImplementation() private returns (bytes memory) { + _requireZeroValue(); + + address implementation = _implementation(); + return abi.encode(implementation); + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + */ + function _dispatchChangeAdmin() private returns (bytes memory) { + _requireZeroValue(); + + address newAdmin = abi.decode(msg.data[4:], (address)); + _changeAdmin(newAdmin); + + return ""; + } + + /** + * @dev Upgrade the implementation of the proxy. + */ + function _dispatchUpgradeTo() private returns (bytes memory) { + _requireZeroValue(); + + address newImplementation = abi.decode(msg.data[4:], (address)); + _upgradeToAndCall(newImplementation, bytes(""), false); + + return ""; + } + + /** + * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified + * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the + * proxied contract. + */ + function _dispatchUpgradeToAndCall() private returns (bytes memory) { + (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes)); + _upgradeToAndCall(newImplementation, data, true); + + return ""; + } + + /** + * @dev Returns the current admin. + * + * CAUTION: This function is deprecated. Use {ERC1967Upgrade-_getAdmin} instead. + */ + function _admin() internal view virtual returns (address) { + return _getAdmin(); + } + + /** + * @dev To keep this contract fully transparent, all `ifAdmin` functions must be payable. This helper is here to + * emulate some proxy functions being non-payable while still allowing value to pass through. + */ + function _requireZeroValue() internal { + require(msg.value == 0); + } +} diff --git a/src/tokens/ERC1155/ERC1155Token.sol b/src/tokens/ERC1155/ERC1155Token.sol new file mode 100644 index 0000000..29594a8 --- /dev/null +++ b/src/tokens/ERC1155/ERC1155Token.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC1155, ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; +import {ERC1155Meta} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155Meta.sol"; +import {ERC1155Metadata} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155Metadata.sol"; +import {ERC2981Controlled} from "@0xsequence/contracts-library/tokens/common/ERC2981Controlled.sol"; + +error InvalidInitialization(); + +/** + * A standard base implementation of ERC-1155 for use in Sequence library contracts. + */ +abstract contract ERC1155Token is ERC1155MintBurn, ERC1155Meta, ERC1155Metadata, ERC2981Controlled { + bytes32 public constant METADATA_ADMIN_ROLE = keccak256("METADATA_ADMIN_ROLE"); + + /** + * Deploy contract. + */ + constructor() ERC1155Metadata("", "") {} + + /** + * Initialize the contract. + * @param owner Owner address. + * @param tokenName Token name. + * @param tokenBaseURI Base URI for token metadata. + * @dev This should be called immediately after deployment. + */ + function _initialize(address owner, string memory tokenName, string memory tokenBaseURI) internal { + name = tokenName; + baseURI = tokenBaseURI; + + _setupRole(DEFAULT_ADMIN_ROLE, owner); + _setupRole(ROYALTY_ADMIN_ROLE, owner); + _setupRole(METADATA_ADMIN_ROLE, owner); + } + + // + // Metadata + // + + /** + * Update the base URL of token's URI. + * @param tokenBaseURI New base URL of token's URI + */ + function setBaseMetadataURI(string memory tokenBaseURI) external onlyRole(METADATA_ADMIN_ROLE) { + _setBaseMetadataURI(tokenBaseURI); + } + + /** + * Update the name of the contract. + * @param tokenName New contract name + */ + function setContractName(string memory tokenName) external onlyRole(METADATA_ADMIN_ROLE) { + _setContractName(tokenName); + } + + // + // Views + // + + /** + * Check interface support. + * @param interfaceId Interface id + * @return True if supported + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override (ERC1155, ERC1155Metadata, ERC2981Controlled) + returns (bool) + { + return ERC1155.supportsInterface(interfaceId) || ERC1155Metadata.supportsInterface(interfaceId) + || ERC2981Controlled.supportsInterface(interfaceId) || super.supportsInterface(interfaceId); + } +} diff --git a/src/tokens/ERC1155/README.md b/src/tokens/ERC1155/README.md new file mode 100644 index 0000000..6e495be --- /dev/null +++ b/src/tokens/ERC1155/README.md @@ -0,0 +1,39 @@ +# ERC1155 Contracts + +This subsection contains contracts related to the [ERC1155 token standard](https://eips.ethereum.org/EIPS/eip-1155). + +## ERC1155Token + +This contract is a base implementation of the ERC-1155 token standard. It includes role based access control features from the [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/4.x/access-control) contract, to provide control over added features. Please refer to OpenZeppelin documentation for more information on AccessControl. + +This contracts provide minting capabilities, support for meta transactions, and metadata functionality. It includes additional features from the ERC1155MintBurn, ERC1155Meta, and ERC1155Metadata contracts. Meta transactions are provided by the [0xSequence ERC1155 library](https://github.com/0xsequence/erc-1155/blob/master/SPECIFICATIONS.md#meta-transactions). Please refer to library documentation for more information on meta transactions. + +The contract supports the [ERC2981 token royalty standard](https://eips.ethereum.org/EIPS/eip-2981) via the ERC2981Controlled contract. Please refer to the ERC2981Controlled documentation for more information on token royalty. + +## Presets + +This folder contains contracts that are pre-configured for specific use cases. + +### Minter + +The `ERC1155TokenMinter` contract is a preset that configures the `ERC1155Token` contract to allow minting of tokens. It adds a `MINTER_ROLE` and a `mint(address to, uint256 amount)` function that can only be called by accounts with the `MINTER_ROLE`. + +### Sale + +The `ERC1155TokenSale` contract is a preset that configures the `ERC1155Token` contract to allow for the sale of tokens. It adds a `mint(address to, , uint256[] memory tokenIds, uint256[] memory amounts, bytes memory data, bytes32[] calldata proof)` function allows for the minting of tokens under various conditions. + +Conditions may be set by the contract owner using either the `setTokenSaleDetails(uint256 tokenId, uint256 cost, uint256 supplyCap, uint64 startTime, uint64 endTime, bytes32 merkleRoot)` function for single token settings or the `setGlobalSaleDetails(uint256 cost, uint256 supplyCap, address paymentTokenAddr, uint64 startTime, uint64 endTime, bytes32 merkleRoot)` function for global settings. These functions can only be called by accounts with the `MINT_ADMIN_ROLE`. + +For information about the function parameters, please refer to the function specification in `presets/sale/IERC1155Sale.sol`. + +## Usage + +This section of this repo utilitizes a factory pattern that deploys proxies contracts. This allows for a single deployment of each `Factory` contract, and subsequent deployments of the contracts with minimal gas costs. + +1. Deploy the `[XXX]Factory` contract for the contract you wish to use (or use an existing deployment). +2. Call the `deploy` function on the factory, providing the desired parameters. +3. A new contract will be created and initialized, ready for use. + +## Dependencies + +This repository relies on the ERC1155, ERC1155MintBurn, ERC1155Meta, ERC1155Metadata contracts from 0xSequence for core ERC-1155 functionality, AccessControl from OpenZeppelin for role base permissions and the ERC2981Controlled contract for handling of royalties. diff --git a/src/tokens/ERC1155/extensions/supply/ERC1155Supply.sol b/src/tokens/ERC1155/extensions/supply/ERC1155Supply.sol new file mode 100644 index 0000000..6920dfa --- /dev/null +++ b/src/tokens/ERC1155/extensions/supply/ERC1155Supply.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC1155Token} from "@0xsequence/contracts-library/tokens/ERC1155/ERC1155Token.sol"; +import {IERC1155Supply} from "@0xsequence/contracts-library/tokens/ERC1155/extensions/supply/IERC1155Supply.sol"; + +/** + * An ERC-1155 extension that tracks token supply. + */ +abstract contract ERC1155Supply is ERC1155Token, IERC1155Supply { + // Maximum supply globaly and per token. 0 indicates unlimited supply + uint256 internal totalSupplyCap; + mapping(uint256 => uint256) internal tokenSupplyCap; + + // Current supply + uint256 public totalSupply; + mapping(uint256 => uint256) public tokenSupply; + + /** + * Mint _amount of tokens of a given id + * @param _to The address to mint tokens to + * @param _id Token id to mint + * @param _amount The amount to be minted + * @param _data Data to pass if receiver is contract + */ + function _mint(address _to, uint256 _id, uint256 _amount, bytes memory _data) internal virtual override { + // Check supply cap + if (totalSupplyCap > 0 && totalSupply + _amount > totalSupplyCap) { + revert InsufficientSupply(totalSupply, _amount, totalSupplyCap); + } + totalSupply += _amount; + if (tokenSupplyCap[_id] > 0 && tokenSupply[_id] + _amount > tokenSupplyCap[_id]) { + revert InsufficientSupply(tokenSupply[_id], _amount, tokenSupplyCap[_id]); + } + tokenSupply[_id] += _amount; + + _mint(_to, _id, _amount, _data); + } + + /** + * Mint tokens for each ids in _ids + * @param _to The address to mint tokens to + * @param _ids Array of ids to mint + * @param _amounts Array of amount of tokens to mint per id + * @param _data Data to pass if receiver is contract + */ + function _batchMint(address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) internal virtual override { + uint256 nMint = _ids.length; + if (nMint != _amounts.length) { + revert InvalidArrayLength(); + } + + // Executing all minting + uint256 totalAmount = 0; + for (uint256 i = 0; i < nMint; i++) { + // Update storage balance + if (tokenSupplyCap[_ids[i]] > 0 && tokenSupply[_ids[i]] + _amounts[i] > tokenSupplyCap[_ids[i]]) { + revert InsufficientSupply(tokenSupply[_ids[i]], _amounts[i], tokenSupplyCap[_ids[i]]); + } + balances[_to][_ids[i]] += _amounts[i]; + tokenSupply[_ids[i]] += _amounts[i]; + totalAmount += _amounts[i]; + } + if (totalSupplyCap > 0 && totalSupply + totalAmount > totalSupplyCap) { + revert InsufficientSupply(totalSupply, totalAmount, totalSupplyCap); + } + totalSupply += totalAmount; + + // Emit batch mint event + emit TransferBatch(msg.sender, address(0x0), _to, _ids, _amounts); + + // Calling onReceive method if recipient is contract + _callonERC1155BatchReceived(address(0x0), _to, _ids, _amounts, gasleft(), _data); + } + + /** + * Burn _amount of tokens of a given token id + * @param _from The address to burn tokens from + * @param _id Token id to burn + * @param _amount The amount to be burned + */ + function _burn(address _from, uint256 _id, uint256 _amount) internal virtual override { + // Supply + totalSupply -= _amount; + tokenSupply[_id] -= _amount; + + // Balances + balances[_from][_id] -= _amount; + + // Emit event + emit TransferSingle(msg.sender, _from, address(0x0), _id, _amount); + } + + /** + * Burn tokens of given token id for each (_ids[i], _amounts[i]) pair + * @param _from The address to burn tokens from + * @param _ids Array of token ids to burn + * @param _amounts Array of the amount to be burned + */ + function _batchBurn(address _from, uint256[] memory _ids, uint256[] memory _amounts) internal virtual override { + // Number of mints to execute + uint256 nBurn = _ids.length; + if (nBurn != _amounts.length) { + revert InvalidArrayLength(); + } + + // Executing all minting + for (uint256 i = 0; i < nBurn; i++) { + // Update balances + balances[_from][_ids[i]] -= _amounts[i]; + totalSupply -= _amounts[i]; + tokenSupply[_ids[i]] -= _amounts[i]; + } + + // Emit batch mint event + emit TransferBatch(msg.sender, _from, address(0x0), _ids, _amounts); + } +} diff --git a/src/tokens/ERC1155/extensions/supply/IERC1155Supply.sol b/src/tokens/ERC1155/extensions/supply/IERC1155Supply.sol new file mode 100644 index 0000000..365383d --- /dev/null +++ b/src/tokens/ERC1155/extensions/supply/IERC1155Supply.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC1155SupplySignals { + + /** + * Insufficient supply of tokens. + */ + error InsufficientSupply(uint256 currentSupply, uint256 requestedAmount, uint256 maxSupply); + + /** + * Invalid array input length. + */ + error InvalidArrayLength(); +} + +interface IERC1155Supply is IERC1155SupplySignals {} diff --git a/src/tokens/ERC1155/presets/minter/ERC1155TokenMinter.sol b/src/tokens/ERC1155/presets/minter/ERC1155TokenMinter.sol new file mode 100644 index 0000000..4c828d5 --- /dev/null +++ b/src/tokens/ERC1155/presets/minter/ERC1155TokenMinter.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC1155MintBurn, ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; +import {IERC1155TokenMinter} from "@0xsequence/contracts-library/tokens/ERC1155//presets/minter/IERC1155TokenMinter.sol"; +import {ERC1155Token} from "@0xsequence/contracts-library/tokens/ERC1155/ERC1155Token.sol"; +import {ERC2981Controlled} from "@0xsequence/contracts-library/tokens/common/ERC2981Controlled.sol"; + +/** + * An implementation of ERC-1155 capable of minting when role provided. + */ +contract ERC1155TokenMinter is ERC1155MintBurn, ERC1155Token, IERC1155TokenMinter { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + address private immutable initializer; + bool private initialized; + + constructor() { + initializer = msg.sender; + } + + /** + * Initialize the contract. + * @param owner Owner address + * @param tokenName Token name + * @param tokenBaseURI Base URI for token metadata + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @dev This should be called immediately after deployment. + */ + function initialize( + address owner, + string memory tokenName, + string memory tokenBaseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + public + virtual + { + if (msg.sender != initializer || initialized) { + revert InvalidInitialization(); + } + + ERC1155Token._initialize(owner, tokenName, tokenBaseURI); + _setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + + _setupRole(MINTER_ROLE, owner); + + initialized = true; + } + + // + // Minting + // + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param tokenId Token ID to mint. + * @param amount Amount of tokens to mint. + * @param data Data to pass if receiver is contract. + */ + function mint(address to, uint256 tokenId, uint256 amount, bytes memory data) external onlyRole(MINTER_ROLE) { + _mint(to, tokenId, amount, data); + } + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param tokenIds Token IDs to mint. + * @param amounts Amounts of tokens to mint. + * @param data Data to pass if receiver is contract. + */ + function batchMint(address to, uint256[] memory tokenIds, uint256[] memory amounts, bytes memory data) + external + onlyRole(MINTER_ROLE) + { + _batchMint(to, tokenIds, amounts, data); + } + + // + // Views + // + + /** + * Check interface support. + * @param interfaceId Interface id + * @return True if supported + */ + function supportsInterface(bytes4 interfaceId) public view override (ERC1155Token, ERC1155) returns (bool) { + return ERC1155Token.supportsInterface(interfaceId); + } +} diff --git a/src/tokens/ERC1155/presets/minter/ERC1155TokenMinterFactory.sol b/src/tokens/ERC1155/presets/minter/ERC1155TokenMinterFactory.sol new file mode 100644 index 0000000..b74cd02 --- /dev/null +++ b/src/tokens/ERC1155/presets/minter/ERC1155TokenMinterFactory.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC1155TokenMinter} from "@0xsequence/contracts-library/tokens/ERC1155/presets/minter/ERC1155TokenMinter.sol"; +import {IERC1155TokenMinterFactory} from + "@0xsequence/contracts-library/tokens/ERC1155/presets/minter/IERC1155TokenMinterFactory.sol"; +import {SequenceProxyFactory} from "@0xsequence/contracts-library/proxies/SequenceProxyFactory.sol"; + +/** + * Deployer of ERC-1155 Token Minter proxies. + */ +contract ERC1155TokenMinterFactory is IERC1155TokenMinterFactory, SequenceProxyFactory { + /** + * Creates an ERC-1155 Token Minter Factory. + * @param factoryOwner The owner of the ERC-1155 Token Minter Factory + */ + constructor(address factoryOwner) { + ERC1155TokenMinter impl = new ERC1155TokenMinter(); + SequenceProxyFactory._initialize(address(impl), factoryOwner); + } + + /** + * Creates an ERC-1155 Token Minter proxy. + * @param proxyOwner The owner of the ERC-1155 Token Minter proxy + * @param tokenOwner The owner of the ERC-1155 Token Minter implementation + * @param name The name of the ERC-1155 Token Minter proxy + * @param baseURI The base URI of the ERC-1155 Token Minter proxy + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @return proxyAddr The address of the ERC-1155 Token Minter Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-1155 Token Minter functions. + */ + function deploy( + address proxyOwner, + address tokenOwner, + string memory name, + string memory baseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + external + returns (address proxyAddr) + { + bytes32 salt = keccak256(abi.encodePacked(tokenOwner, name, baseURI, royaltyReceiver, royaltyFeeNumerator)); + proxyAddr = _createProxy(salt, proxyOwner, ""); + ERC1155TokenMinter(proxyAddr).initialize(tokenOwner, name, baseURI, royaltyReceiver, royaltyFeeNumerator); + emit ERC1155TokenMinterDeployed(proxyAddr); + return proxyAddr; + } +} diff --git a/src/tokens/ERC1155/presets/minter/IERC1155TokenMinter.sol b/src/tokens/ERC1155/presets/minter/IERC1155TokenMinter.sol new file mode 100644 index 0000000..a4a9250 --- /dev/null +++ b/src/tokens/ERC1155/presets/minter/IERC1155TokenMinter.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC1155TokenMinterFunctions { + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param tokenId Token ID to mint. + * @param amount Amount of tokens to mint. + * @param data Data to pass if receiver is contract. + */ + function mint(address to, uint256 tokenId, uint256 amount, bytes memory data) external; + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param tokenIds Token IDs to mint. + * @param amounts Amounts of tokens to mint. + * @param data Data to pass if receiver is contract. + */ + function batchMint(address to, uint256[] memory tokenIds, uint256[] memory amounts, bytes memory data) external; +} + +interface IERC1155TokenMinterSignals { + /** + * Invalid initialization error. + */ + error InvalidInitialization(); +} + +interface IERC1155TokenMinter is IERC1155TokenMinterFunctions, IERC1155TokenMinterSignals {} diff --git a/src/tokens/ERC1155/presets/minter/IERC1155TokenMinterFactory.sol b/src/tokens/ERC1155/presets/minter/IERC1155TokenMinterFactory.sol new file mode 100644 index 0000000..b4da515 --- /dev/null +++ b/src/tokens/ERC1155/presets/minter/IERC1155TokenMinterFactory.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC1155TokenMinterFactoryFunctions { + /** + * Creates an ERC-1155 Token Minter proxy. + * @param proxyOwner The owner of the ERC-1155 Token Minter proxy + * @param tokenOwner The owner of the ERC-1155 Token Minter implementation + * @param name The name of the ERC-1155 Token Minter proxy + * @param baseURI The base URI of the ERC-1155 Token Minter proxy + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @return proxyAddr The address of the ERC-1155 Token Minter Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-1155 Token Minter functions. + */ + function deploy( + address proxyOwner, + address tokenOwner, + string memory name, + string memory baseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + external + returns (address proxyAddr); +} + +interface IERC1155TokenMinterFactorySignals { + /** + * Event emitted when a new ERC-1155 Token Minter proxy contract is deployed. + * @param proxyAddr The address of the deployed proxy. + */ + event ERC1155TokenMinterDeployed(address proxyAddr); +} + +interface IERC1155TokenMinterFactory is IERC1155TokenMinterFactoryFunctions, IERC1155TokenMinterFactorySignals {} diff --git a/src/tokens/ERC1155/presets/sale/ERC1155Sale.sol b/src/tokens/ERC1155/presets/sale/ERC1155Sale.sol new file mode 100644 index 0000000..baac0a8 --- /dev/null +++ b/src/tokens/ERC1155/presets/sale/ERC1155Sale.sol @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {IERC1155Sale} from "@0xsequence/contracts-library/tokens/ERC1155/presets/sale/IERC1155Sale.sol"; +import { + ERC1155Supply, + ERC1155Token +} from "@0xsequence/contracts-library/tokens/ERC1155/extensions/supply/ERC1155Supply.sol"; +import { + WithdrawControlled, + AccessControl, + SafeERC20, + IERC20 +} from "@0xsequence/contracts-library/tokens/common/WithdrawControlled.sol"; +import {MerkleProofSingleUse} from "@0xsequence/contracts-library/tokens/common/MerkleProofSingleUse.sol"; + +contract ERC1155Sale is IERC1155Sale, ERC1155Supply, WithdrawControlled, MerkleProofSingleUse { + bytes32 public constant MINT_ADMIN_ROLE = keccak256("MINT_ADMIN_ROLE"); + + bytes4 private constant _ERC20_TRANSFERFROM_SELECTOR = + bytes4(keccak256(bytes("transferFrom(address,address,uint256)"))); + + bool private _initialized; + + // ERC20 token address for payment. address(0) indicated payment in ETH. + address private _paymentToken; + + SaleDetails private _globalSaleDetails; + mapping(uint256 => SaleDetails) private _tokenSaleDetails; + + /** + * Initialize the contract. + * @param owner Owner address + * @param tokenName Token name + * @param tokenBaseURI Base URI for token metadata + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @dev This should be called immediately after deployment. + */ + function initialize( + address owner, + string memory tokenName, + string memory tokenBaseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + public + virtual + { + if (_initialized) { + revert InvalidInitialization(); + } + + ERC1155Token._initialize(owner, tokenName, tokenBaseURI); + _setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + + _setupRole(MINT_ADMIN_ROLE, owner); + _setupRole(WITHDRAW_ROLE, owner); + + _initialized = true; + } + + /** + * Checks if the current block.timestamp is out of the give timestamp range. + * @param _startTime Earliest acceptable timestamp (inclusive). + * @param _endTime Latest acceptable timestamp (exclusive). + * @dev A zero endTime value is always considered out of bounds. + */ + function blockTimeOutOfBounds(uint256 _startTime, uint256 _endTime) private view returns (bool) { + // 0 end time indicates inactive sale. + return _endTime == 0 || block.timestamp < _startTime || block.timestamp >= _endTime; // solhint-disable-line not-rely-on-time + } + + /** + * Checks the sale is active and takes payment. + * @param _tokenIds Token IDs to mint. + * @param _amounts Amounts of tokens to mint. + * @param _proof Merkle proof for allowlist minting. + */ + function _payForActiveMint(uint256[] memory _tokenIds, uint256[] memory _amounts, bytes32[] calldata _proof) + private + { + uint256 lastTokenId; + uint256 totalCost; + uint256 totalAmount; + + SaleDetails memory gSaleDetails = _globalSaleDetails; + bool globalSaleInactive = blockTimeOutOfBounds(gSaleDetails.startTime, gSaleDetails.endTime); + for (uint256 i; i < _tokenIds.length; i++) { + uint256 tokenId = _tokenIds[i]; + // Test tokenIds ordering + if (i != 0 && lastTokenId >= tokenId) { + revert InvalidTokenIds(); + } + lastTokenId = tokenId; + + uint256 amount = _amounts[i]; + + // Active sale test + SaleDetails memory saleDetails = _tokenSaleDetails[tokenId]; + bool tokenSaleInactive = blockTimeOutOfBounds(saleDetails.startTime, saleDetails.endTime); + if (tokenSaleInactive) { + // Prefer token sale + if (globalSaleInactive) { + // Both sales inactive + revert SaleInactive(tokenId); + } + // Use global sale details + requireMerkleProof(gSaleDetails.merkleRoot, _proof, msg.sender); + totalCost += gSaleDetails.cost * amount; + } else { + // Use token sale details + requireMerkleProof(saleDetails.merkleRoot, _proof, msg.sender); + totalCost += saleDetails.cost * amount; + } + totalAmount += amount; + } + + if (_paymentToken == address(0)) { + // Paid in ETH + if (msg.value != totalCost) { + revert InsufficientPayment(totalCost, msg.value); + } + } else { + // Paid in ERC20 + SafeERC20.safeTransferFrom(IERC20(_paymentToken), msg.sender, address(this), totalCost); + } + } + + // + // Minting + // + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param tokenIds Token IDs to mint. + * @param amounts Amounts of tokens to mint. + * @param data Data to pass if receiver is contract. + * @param proof Merkle proof for allowlist minting. + * @notice Sale must be active for all tokens. + * @dev tokenIds must be sorted ascending without duplicates. + * @dev An empty proof is supplied when no proof is required. + */ + function mint( + address to, + uint256[] memory tokenIds, + uint256[] memory amounts, + bytes memory data, + bytes32[] calldata proof + ) + public + payable + { + _payForActiveMint(tokenIds, amounts, proof); + _batchMint(to, tokenIds, amounts, data); + } + + /** + * Mint tokens as admin. + * @param to Address to mint tokens to. + * @param tokenIds Token IDs to mint. + * @param amounts Amounts of tokens to mint. + * @param data Data to pass if receiver is contract. + * @notice Only callable by mint admin. + */ + function mintAdmin(address to, uint256[] memory tokenIds, uint256[] memory amounts, bytes memory data) + public + onlyRole(MINT_ADMIN_ROLE) + { + _batchMint(to, tokenIds, amounts, data); + } + + /** + * Set the global sale details. + * @param cost The amount of payment tokens to accept for each token minted. + * @param supplyCap The maximum number of tokens that can be minted. + * @param paymentTokenAddr The ERC20 token address to accept payment in. address(0) indicates ETH. + * @param startTime The start time of the sale. Tokens cannot be minted before this time. + * @param endTime The end time of the sale. Tokens cannot be minted after this time. + * @param merkleRoot The merkle root for allowlist minting. + * @dev A zero end time indicates an inactive sale. + */ + function setGlobalSaleDetails( + uint256 cost, + uint256 supplyCap, + address paymentTokenAddr, + uint64 startTime, + uint64 endTime, + bytes32 merkleRoot + ) + public + onlyRole(MINT_ADMIN_ROLE) + { + _paymentToken = paymentTokenAddr; + _globalSaleDetails = SaleDetails(cost, startTime, endTime, merkleRoot); + totalSupplyCap = supplyCap; + emit GlobalSaleDetailsUpdated(cost, supplyCap, startTime, endTime, merkleRoot); + } + + /** + * Set the sale details for an individual token. + * @param tokenId The token ID to set the sale details for. + * @param cost The amount of payment tokens to accept for each token minted. + * @param supplyCap The maximum number of tokens that can be minted. + * @param startTime The start time of the sale. Tokens cannot be minted before this time. + * @param endTime The end time of the sale. Tokens cannot be minted after this time. + * @param merkleRoot The merkle root for allowlist minting. + * @dev A zero end time indicates an inactive sale. + * @notice The payment token is set globally. + */ + function setTokenSaleDetails( + uint256 tokenId, + uint256 cost, + uint256 supplyCap, + uint64 startTime, + uint64 endTime, + bytes32 merkleRoot + ) + public + onlyRole(MINT_ADMIN_ROLE) + { + _tokenSaleDetails[tokenId] = SaleDetails(cost, startTime, endTime, merkleRoot); + tokenSupplyCap[tokenId] = supplyCap; + emit TokenSaleDetailsUpdated(tokenId, cost, supplyCap, startTime, endTime, merkleRoot); + } + + // + // Views + // + + /** + * Get global sales details. + * @return Sale details. + * @notice Global sales details apply to all tokens. + * @notice Global sales details are overriden when token sale is active. + */ + function globalSaleDetails() external view returns (SaleDetails memory) { + return _globalSaleDetails; + } + + /** + * Get token sale details. + * @param tokenId Token ID to get sale details for. + * @return Sale details. + * @notice Token sale details override global sale details. + */ + function tokenSaleDetails(uint256 tokenId) external view returns (SaleDetails memory) { + return _tokenSaleDetails[tokenId]; + } + + /** + * Get payment token. + * @return Payment token address. + * @notice address(0) indicates payment in ETH. + */ + function paymentToken() external view returns (address) { + return _paymentToken; + } + + /** + * Check interface support. + * @param interfaceId Interface id + * @return True if supported + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override (ERC1155Token, AccessControl) + returns (bool) + { + return interfaceId == type(IERC1155Sale).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/src/tokens/ERC1155/presets/sale/ERC1155SaleFactory.sol b/src/tokens/ERC1155/presets/sale/ERC1155SaleFactory.sol new file mode 100644 index 0000000..74f0263 --- /dev/null +++ b/src/tokens/ERC1155/presets/sale/ERC1155SaleFactory.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC1155Sale} from "@0xsequence/contracts-library/tokens/ERC1155/presets/sale/ERC1155Sale.sol"; +import {IERC1155SaleFactory} from "@0xsequence/contracts-library/tokens/ERC1155/presets/sale/IERC1155SaleFactory.sol"; +import {SequenceProxyFactory} from "@0xsequence/contracts-library/proxies/SequenceProxyFactory.sol"; + +/** + * Deployer of ERC-1155 Sale proxies. + */ +contract ERC1155SaleFactory is IERC1155SaleFactory, SequenceProxyFactory { + /** + * Creates an ERC-1155 Sale Factory. + * @param factoryOwner The owner of the ERC-1155 Sale Factory + */ + constructor(address factoryOwner) { + ERC1155Sale impl = new ERC1155Sale(); + SequenceProxyFactory._initialize(address(impl), factoryOwner); + } + + /** + * Creates an ERC-1155 Sale proxy contract + * @param proxyOwner The owner of the ERC-1155 Sale proxy + * @param tokenOwner The owner of the ERC-1155 Sale implementation + * @param name The name of the ERC-1155 Sale token + * @param baseURI The base URI of the ERC-1155 Sale token + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @return proxyAddr The address of the ERC-1155 Sale Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-1155 Sale Minter functions. + */ + function deploy( + address proxyOwner, + address tokenOwner, + string memory name, + string memory baseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + external + returns (address proxyAddr) + { + bytes32 salt = keccak256(abi.encodePacked(tokenOwner, name, baseURI, royaltyReceiver, royaltyFeeNumerator)); + proxyAddr = _createProxy(salt, proxyOwner, ""); + ERC1155Sale(proxyAddr).initialize(tokenOwner, name, baseURI, royaltyReceiver, royaltyFeeNumerator); + emit ERC1155SaleDeployed(proxyAddr); + return proxyAddr; + } +} diff --git a/src/tokens/ERC1155/presets/sale/IERC1155Sale.sol b/src/tokens/ERC1155/presets/sale/IERC1155Sale.sol new file mode 100644 index 0000000..50af47b --- /dev/null +++ b/src/tokens/ERC1155/presets/sale/IERC1155Sale.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC1155SaleFunctions { + + struct SaleDetails { + uint256 cost; + uint64 startTime; + uint64 endTime; // 0 end time indicates sale inactive + bytes32 merkleRoot; // Root of allowed addresses + } + + /** + * Get global sales details. + * @return Sale details. + * @notice Global sales details apply to all tokens. + * @notice Global sales details are overriden when token sale is active. + */ + function globalSaleDetails() external returns (SaleDetails memory); + + /** + * Get token sale details. + * @param tokenId Token ID to get sale details for. + * @return Sale details. + * @notice Token sale details override global sale details. + */ + function tokenSaleDetails(uint256 tokenId) external returns (SaleDetails memory); + + /** + * Get payment token. + * @return Payment token address. + * @notice address(0) indicates payment in ETH. + */ + function paymentToken() external returns (address); + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param tokenIds Token IDs to mint. + * @param amounts Amounts of tokens to mint. + * @param data Data to pass if receiver is contract. + * @param proof Merkle proof for allowlist minting. + * @notice Sale must be active for all tokens. + */ + function mint( + address to, + uint256[] memory tokenIds, + uint256[] memory amounts, + bytes memory data, + bytes32[] calldata proof + ) + external + payable; +} + +interface IERC1155SaleSignals { + + event GlobalSaleDetailsUpdated(uint256 cost, uint256 supplyCap, uint64 startTime, uint64 endTime, bytes32 merkleRoot); + event TokenSaleDetailsUpdated(uint256 tokenId, uint256 cost, uint256 supplyCap, uint64 startTime, uint64 endTime, bytes32 merkleRoot); + + /** + * Contract already initialized. + */ + error InvalidInitialization(); + + /** + * Sale is not active globally. + */ + error GlobalSaleInactive(); + + /** + * Sale is not active. + * @param tokenId Invalid Token ID. + */ + error SaleInactive(uint256 tokenId); + + /** + * Insufficient tokens for payment. + * @param expected Expected amount of tokens. + * @param actual Actual amount of tokens. + */ + error InsufficientPayment(uint256 expected, uint256 actual); + + /** + * Invalid token IDs. + */ + error InvalidTokenIds(); +} + +interface IERC1155Sale is IERC1155SaleFunctions, IERC1155SaleSignals {} diff --git a/src/tokens/ERC1155/presets/sale/IERC1155SaleFactory.sol b/src/tokens/ERC1155/presets/sale/IERC1155SaleFactory.sol new file mode 100644 index 0000000..3ebb10e --- /dev/null +++ b/src/tokens/ERC1155/presets/sale/IERC1155SaleFactory.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC1155SaleFactoryFunctions { + /** + * Creates an ERC-1155 Sale proxy contract + * @param proxyOwner The owner of the ERC-1155 Sale proxy + * @param tokenOwner The owner of the ERC-1155 Sale implementation + * @param name The name of the ERC-1155 Sale token + * @param baseURI The base URI of the ERC-1155 Sale token + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @return proxyAddr The address of the ERC-1155 Sale Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-1155 Token Sale functions. + */ + function deploy( + address proxyOwner, + address tokenOwner, + string memory name, + string memory baseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + external + returns (address proxyAddr); +} + +interface IERC1155SaleFactorySignals { + /** + * Event emitted when a new ERC-1155 Sale proxy contract is deployed. + * @param proxyAddr The address of the deployed proxy. + */ + event ERC1155SaleDeployed(address proxyAddr); +} + +interface IERC1155SaleFactory is IERC1155SaleFactoryFunctions, IERC1155SaleFactorySignals {} diff --git a/src/tokens/ERC20/ERC20Token.sol b/src/tokens/ERC20/ERC20Token.sol new file mode 100644 index 0000000..d17ff66 --- /dev/null +++ b/src/tokens/ERC20/ERC20Token.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +error InvalidInitialization(); + +/** + * A standard base implementation of ERC-20 for use in Sequence library contracts. + */ +abstract contract ERC20Token is ERC20, AccessControl { + + string internal _tokenName; + string internal _tokenSymbol; + uint8 private _tokenDecimals; + + address private immutable _initializer; + bool private _initialized; + + constructor() ERC20("", "") { + _initializer = msg.sender; + } + + /** + * Initialize contract. + * @param owner The owner of the contract + * @param tokenName Name of the token + * @param tokenSymbol Symbol of the token + * @param tokenDecimals Number of decimals + * @dev This should be called immediately after deployment. + */ + function initialize(address owner, string memory tokenName, string memory tokenSymbol, uint8 tokenDecimals) public virtual { + if (msg.sender != _initializer || _initialized) { + revert InvalidInitialization(); + } + + _tokenName = tokenName; + _tokenSymbol = tokenSymbol; + _tokenDecimals = tokenDecimals; + + _setupRole(DEFAULT_ADMIN_ROLE, owner); + + _initialized = true; + } + + // + // Views + // + + /** + * Check interface support. + * @param interfaceId Interface id + * @return True if supported + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC20).interfaceId || interfaceId == type(IERC20Metadata).interfaceId + || AccessControl.supportsInterface(interfaceId) || super.supportsInterface(interfaceId); + } + + // + // ERC20 Overrides + // + + /** + * Override the ERC20 name function. + */ + function name() public view override returns (string memory) { + return _tokenName; + } + + /** + * Override the ERC20 symbol function. + */ + function symbol() public view override returns (string memory) { + return _tokenSymbol; + } + + /** + * Override the ERC20 decimals function. + */ + function decimals() public view override returns (uint8) { + return _tokenDecimals; + } +} diff --git a/src/tokens/ERC20/README.md b/src/tokens/ERC20/README.md new file mode 100644 index 0000000..2c12642 --- /dev/null +++ b/src/tokens/ERC20/README.md @@ -0,0 +1,27 @@ +# ERC20 Contracts + +This subsection contains contracts related to the [ERC20 token standard](https://eips.ethereum.org/EIPS/eip-20). + +## ERC20Token + +This contract is a base implementation of the ERC-20 token standard. It includes role based access control features from the [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/4.x/access-control) contract, to provide control over added features. Please refer to OpenZeppelin documentation for more information on AccessControl. + +## Presets + +This folder contains contracts that are pre-configured for specific use cases. + +### Minter + +The `ERC20TokenMinter` contract is a preset that configures the `ERC20Token` contract to allow minting of tokens. It adds a `MINTER_ROLE` and a `mint(address to, uint256 amount)` function that can only be called by accounts with the `MINTER_ROLE`. + +## Usage + +This section of this repo utilitizes a factory pattern that deploys proxies contracts. This allows for a single deployment of each `Factory` contract, and subsequent deployments of the contracts with minimal gas costs. + +1. Deploy the `[XXX]Factory` contract for the contract you wish to use (or use an existing deployment). +2. Call the `deploy` function on the factory, providing the desired parameters. +3. A new contract will be created and initialized, ready for use. + +## Dependencies + +These contract relies on the OpenZeppelin Contracts library, particularly the ERC20, IERC20, IERC20Metadata, and AccessControl contracts, which provide core ERC-20 functionality and secure access control mechanisms. diff --git a/src/tokens/ERC20/presets/minter/ERC20TokenMinter.sol b/src/tokens/ERC20/presets/minter/ERC20TokenMinter.sol new file mode 100644 index 0000000..746a8e2 --- /dev/null +++ b/src/tokens/ERC20/presets/minter/ERC20TokenMinter.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC20Token} from "@0xsequence/contracts-library/tokens/ERC20/ERC20Token.sol"; +import {IERC20TokenMinter} from "@0xsequence/contracts-library/tokens/ERC20/presets/minter/IERC20TokenMinter.sol"; + +/** + * A ready made implementation of ERC-20 capable of minting when role provided. + */ +contract ERC20TokenMinter is ERC20Token, IERC20TokenMinter { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + address private immutable _initializer; + bool private _initialized; + + constructor() { + _initializer = msg.sender; + } + + /** + * Initialize contract. + * @param owner The owner of the contract + * @param tokenName Name of the token + * @param tokenSymbol Symbol of the token + * @param tokenDecimals Number of decimals + * @dev This should be called immediately after deployment. + */ + function initialize(address owner, string memory tokenName, string memory tokenSymbol, uint8 tokenDecimals) public virtual override { + if (msg.sender != _initializer || _initialized) { + revert InvalidInitialization(); + } + + ERC20Token.initialize(owner, tokenName, tokenSymbol, tokenDecimals); + + _setupRole(MINTER_ROLE, owner); + + _initialized = true; + } + + // + // Minting + // + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param amount Amount of tokens to mint. + * @notice This function can only be called by a minter. + */ + function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + // + // Admin + // + + /** + * Set name and symbol of token. + * @param tokenName Name of token. + * @param tokenSymbol Symbol of token. + */ + function setNameAndSymbol(string memory tokenName, string memory tokenSymbol) external onlyRole(DEFAULT_ADMIN_ROLE) { + _tokenName = tokenName; + _tokenSymbol = tokenSymbol; + } + + // + // Views + // + + /** + * Check interface support. + * @param interfaceId Interface id + * @return True if supported + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return ERC20Token.supportsInterface(interfaceId) || super.supportsInterface(interfaceId); + } +} diff --git a/src/tokens/ERC20/presets/minter/ERC20TokenMinterFactory.sol b/src/tokens/ERC20/presets/minter/ERC20TokenMinterFactory.sol new file mode 100644 index 0000000..fe0fdd0 --- /dev/null +++ b/src/tokens/ERC20/presets/minter/ERC20TokenMinterFactory.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC20TokenMinter} from "@0xsequence/contracts-library/tokens/ERC20/presets/minter/ERC20TokenMinter.sol"; +import {IERC20TokenMinterFactory} from + "@0xsequence/contracts-library/tokens/ERC20/presets/minter/IERC20TokenMinterFactory.sol"; +import {SequenceProxyFactory} from "@0xsequence/contracts-library/proxies/SequenceProxyFactory.sol"; + +/** + * Deployer of ERC-20 Token Minter proxies. + */ +contract ERC20TokenMinterFactory is IERC20TokenMinterFactory, SequenceProxyFactory { + /** + * Creates an ERC-20 Token Minter Factory. + * @param factoryOwner The owner of the ERC-20 Token Minter Factory + */ + constructor(address factoryOwner) { + ERC20TokenMinter impl = new ERC20TokenMinter(); + SequenceProxyFactory._initialize(address(impl), factoryOwner); + } + + /** + * Creates an ERC-20 Token Minter proxy. + * @param proxyOwner The owner of the ERC-20 Token Minter proxy + * @param tokenOwner The owner of the ERC-20 Token Minter implementation + * @param name The name of the ERC-20 Token Minter proxy + * @param symbol The symbol of the ERC-20 Token Minter proxy + * @param decimals The decimals of the ERC-20 Token Minter proxy + * @return proxyAddr The address of the ERC-20 Token Minter Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-20 Token Minter functions. + */ + function deploy(address proxyOwner, address tokenOwner, string memory name, string memory symbol, uint8 decimals) + external + returns (address proxyAddr) + { + bytes32 salt = keccak256(abi.encodePacked(tokenOwner, name, symbol, decimals)); + proxyAddr = _createProxy(salt, proxyOwner, ""); + ERC20TokenMinter(proxyAddr).initialize(tokenOwner, name, symbol, decimals); + emit ERC20TokenMinterDeployed(proxyAddr); + return proxyAddr; + } +} diff --git a/src/tokens/ERC20/presets/minter/IERC20TokenMinter.sol b/src/tokens/ERC20/presets/minter/IERC20TokenMinter.sol new file mode 100644 index 0000000..f9e92d9 --- /dev/null +++ b/src/tokens/ERC20/presets/minter/IERC20TokenMinter.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC20TokenMinterFunctions { + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param amount Amount of tokens to mint. + */ + function mint(address to, uint256 amount) external; + + /** + * Set name and symbol of token. + * @param tokenName Name of token. + * @param tokenSymbol Symbol of token. + */ + function setNameAndSymbol(string memory tokenName, string memory tokenSymbol) external; +} + +interface IERC20TokenMinterSignals { + + /** + * Invalid initialization error. + */ + error InvalidInitialization(); +} + +interface IERC20TokenMinter is IERC20TokenMinterSignals {} diff --git a/src/tokens/ERC20/presets/minter/IERC20TokenMinterFactory.sol b/src/tokens/ERC20/presets/minter/IERC20TokenMinterFactory.sol new file mode 100644 index 0000000..64af83e --- /dev/null +++ b/src/tokens/ERC20/presets/minter/IERC20TokenMinterFactory.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC20TokenMinterFactoryFunctions { + /** + * Creates an ERC-20 Token Minter proxy. + * @param proxyOwner The owner of the ERC-20 Token Minter proxy + * @param tokenOwner The owner of the ERC-20 Token Minter implementation + * @param name The name of the ERC-20 Token Minter proxy + * @param symbol The symbol of the ERC-20 Token Minter proxy + * @param decimals The decimals of the ERC-20 Token Minter proxy + * @return proxyAddr The address of the ERC-20 Token Minter Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-20 Token Minter functions. + */ + function deploy(address proxyOwner, address tokenOwner, string memory name, string memory symbol, uint8 decimals) + external + returns (address proxyAddr); +} + +interface IERC20TokenMinterFactorySignals { + /** + * Event emitted when a new ERC-20 Token Minter proxy contract is deployed. + * @param proxyAddr The address of the deployed proxy. + */ + event ERC20TokenMinterDeployed(address proxyAddr); +} + +interface IERC20TokenMinterFactory is IERC20TokenMinterFactoryFunctions, IERC20TokenMinterFactorySignals {} diff --git a/src/tokens/ERC721/ERC721Token.sol b/src/tokens/ERC721/ERC721Token.sol new file mode 100644 index 0000000..1812acb --- /dev/null +++ b/src/tokens/ERC721/ERC721Token.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import { + ERC721AQueryable, IERC721AQueryable, ERC721A, IERC721A +} from "erc721a/contracts/extensions/ERC721AQueryable.sol"; +import {ERC2981Controlled} from "@0xsequence/contracts-library/tokens/common/ERC2981Controlled.sol"; + +error InvalidInitialization(); + +/** + * A standard base implementation of ERC-721 for use in Sequence library contracts. + */ +abstract contract ERC721Token is ERC721AQueryable, ERC2981Controlled { + bytes32 public constant METADATA_ADMIN_ROLE = keccak256("METADATA_ADMIN_ROLE"); + + string private _tokenBaseURI; + string internal _tokenName; + string internal _tokenSymbol; + + /** + * Deploy contract. + */ + constructor() ERC721A("", "") {} + + /** + * Initialize contract. + * @param owner The owner of the contract + * @param tokenName Name of the token + * @param tokenSymbol Symbol of the token + * @param tokenBaseURI Base URI of the token + * @dev This should be called immediately after deployment. + */ + function _initialize(address owner, string memory tokenName, string memory tokenSymbol, string memory tokenBaseURI) + internal + { + _tokenName = tokenName; + _tokenSymbol = tokenSymbol; + _tokenBaseURI = tokenBaseURI; + + _setupRole(DEFAULT_ADMIN_ROLE, owner); + _setupRole(METADATA_ADMIN_ROLE, owner); + _setupRole(ROYALTY_ADMIN_ROLE, owner); + } + + // + // Metadata + // + + /** + * Update the base URL of token's URI. + * @param tokenBaseURI New base URL of token's URI + */ + function setBaseMetadataURI(string memory tokenBaseURI) external onlyRole(METADATA_ADMIN_ROLE) { + _tokenBaseURI = tokenBaseURI; + } + + // + // Views + // + + /** + * Check interface support. + * @param interfaceId Interface id + * @return True if supported + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override (ERC721A, IERC721A, ERC2981Controlled) + returns (bool) + { + return interfaceId == type(IERC721A).interfaceId || interfaceId == type(IERC721AQueryable).interfaceId + || ERC721A.supportsInterface(interfaceId) || ERC2981Controlled.supportsInterface(interfaceId) + || super.supportsInterface(interfaceId); + } + + // + // ERC721A Overrides + // + + /** + * Override the ERC721A baseURI function. + */ + function _baseURI() internal view override returns (string memory) { + return _tokenBaseURI; + } + + /** + * Override the ERC721A name function. + */ + function name() public view override (ERC721A, IERC721A) returns (string memory) { + return _tokenName; + } + + /** + * Override the ERC721A symbol function. + */ + function symbol() public view override (ERC721A, IERC721A) returns (string memory) { + return _tokenSymbol; + } +} diff --git a/src/tokens/ERC721/README.md b/src/tokens/ERC721/README.md new file mode 100644 index 0000000..e427a57 --- /dev/null +++ b/src/tokens/ERC721/README.md @@ -0,0 +1,42 @@ +# ERC721 Contracts + +This subsection contains contracts related to the [ERC721 token standard](https://eips.ethereum.org/EIPS/eip-721). + +## ERC721Token + +This contract is a base implementation of the ERC-721 token standard. It leverages the [Azuki ERC-721A implementation](https://www.erc721a.org/) for gas efficiency. It includes role based access control features from the [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/4.x/access-control) contract, to provide control over added features. Please refer to OpenZeppelin documentation for more information on AccessControl. + +The contract supports the [ERC2981 token royalty standard](https://eips.ethereum.org/EIPS/eip-2981) via the ERC2981Controlled contract. Please refer to the ERC2981Controlled documentation for more information on token royalty. + +## Presets + +This folder contains contracts that are pre-configured for specific use cases. + +### Minter + +The `ERC721TokenMinter` contract is a preset that configures the `ERC721Token` contract to allow minting of tokens. It adds a `MINTER_ROLE` and a `mint(address to, uint256 amount)` function that can only be called by accounts with the `MINTER_ROLE`. + +### Sale + +The `ERC721TokenSale` contract is a preset that configures the `ERC721Token` contract to allow for the sale of tokens. It adds a `mint(address to, uint256 amount, bytes32[] memory proof)` function allows for the minting of tokens under various conditions. + +Conditions may be set by the contract owner using the `setSaleDetails(uint256 supplyCap, uint256 cost, address paymentToken, uint64 startTime, uint64 endTime, bytes32 merkleRoot)` function that can only be called by accounts with the `MINT_ADMIN_ROLE`. The variables function as follows: + +* supplyCap: The maximum number of tokens that can be minted. 0 indicates unlimited supply. +* cost: The amount of payment tokens to accept for each token minted. +* paymentToken: The ERC20 token address to accept payment in. address(0) indicates ETH. +* startTime: The start time of the sale. Tokens cannot be minted before this time. +* endTime: The end time of the sale. Tokens cannot be minted after this time. +* merkleRoot: The merkle root for allowlist minting. + +## Usage + +This section of this repo utilitizes a factory pattern that deploys proxies contracts. This allows for a single deployment of each `Factory` contract, and subsequent deployments of the contracts with minimal gas costs. + +1. Deploy the `[XXX]Factory` contract for the contract you wish to use (or use an existing deployment). +2. Call the `deploy` function on the factory, providing the desired parameters. +3. A new contract will be created and initialized, ready for use. + +## Dependencies + +This repo relies on the ERC721A, IERC721A, ERC721AQueryable, and IERC721AQueryable contracts from Azuki for core ERC-721 functionality, AccessControl from OpenZeppelin for role base permissions and the ERC2981Controlled contract for handling of royalties. diff --git a/src/tokens/ERC721/presets/minter/ERC721TokenMinter.sol b/src/tokens/ERC721/presets/minter/ERC721TokenMinter.sol new file mode 100644 index 0000000..eac51de --- /dev/null +++ b/src/tokens/ERC721/presets/minter/ERC721TokenMinter.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC721Token} from "@0xsequence/contracts-library/tokens/ERC721/ERC721Token.sol"; +import {IERC721TokenMinter} from "@0xsequence/contracts-library/tokens/ERC721/presets/minter/IERC721TokenMinter.sol"; + +/** + * An implementation of ERC-721 capable of minting when role provided. + */ +contract ERC721TokenMinter is ERC721Token, IERC721TokenMinter { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + address private immutable _initializer; + bool private _initialized; + + /** + * Deploy contract. + */ + constructor() ERC721Token() { + _initializer = msg.sender; + } + + /** + * Initialize contract. + * @param owner The owner of the contract + * @param tokenName Name of the token + * @param tokenSymbol Symbol of the token + * @param tokenBaseURI Base URI of the token + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @dev This should be called immediately after deployment. + */ + function initialize( + address owner, + string memory tokenName, + string memory tokenSymbol, + string memory tokenBaseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + public + virtual + { + if (msg.sender != _initializer || _initialized) { + revert InvalidInitialization(); + } + + ERC721Token._initialize(owner, tokenName, tokenSymbol, tokenBaseURI); + _setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + + _setupRole(MINTER_ROLE, owner); + + _initialized = true; + } + + // + // Minting + // + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param amount Amount of tokens to mint. + */ + function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + // + // Admin + // + + /** + * Set name and symbol of token. + * @param tokenName Name of token. + * @param tokenSymbol Symbol of token. + */ + function setNameAndSymbol(string memory tokenName, string memory tokenSymbol) + external + onlyRole(METADATA_ADMIN_ROLE) + { + _tokenName = tokenName; + _tokenSymbol = tokenSymbol; + } +} diff --git a/src/tokens/ERC721/presets/minter/ERC721TokenMinterFactory.sol b/src/tokens/ERC721/presets/minter/ERC721TokenMinterFactory.sol new file mode 100644 index 0000000..ef03cde --- /dev/null +++ b/src/tokens/ERC721/presets/minter/ERC721TokenMinterFactory.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {IERC721TokenMinterFactory} from + "@0xsequence/contracts-library/tokens/ERC721/presets/minter/IERC721TokenMinterFactory.sol"; +import {ERC721TokenMinter} from "@0xsequence/contracts-library/tokens/ERC721/presets/minter/ERC721TokenMinter.sol"; +import {SequenceProxyFactory} from "@0xsequence/contracts-library/proxies/SequenceProxyFactory.sol"; + +/** + * Deployer of ERC-721 Token Minter proxies. + */ +contract ERC721TokenMinterFactory is IERC721TokenMinterFactory, SequenceProxyFactory { + /** + * Creates an ERC-721 Token Minter Factory. + * @param factoryOwner The owner of the ERC-721 Token Minter Factory + */ + constructor(address factoryOwner) { + ERC721TokenMinter impl = new ERC721TokenMinter(); + SequenceProxyFactory._initialize(address(impl), factoryOwner); + } + + /** + * Creates an ERC-721 Token Minter proxy. + * @param proxyOwner The owner of the ERC-721 Token Minter proxy + * @param tokenOwner The owner of the ERC-721 Token Minter implementation + * @param name The name of the ERC-721 Token Minter proxy + * @param symbol The symbol of the ERC-721 Token Minter proxy + * @param baseURI The base URI of the ERC-721 Token Minter proxy + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @return proxyAddr The address of the ERC-721 Token Minter Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-721 Token Minter functions. + */ + function deploy( + address proxyOwner, + address tokenOwner, + string memory name, + string memory symbol, + string memory baseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + external + returns (address proxyAddr) + { + bytes32 salt = + keccak256(abi.encodePacked(tokenOwner, name, symbol, baseURI, royaltyReceiver, royaltyFeeNumerator)); + proxyAddr = _createProxy(salt, proxyOwner, ""); + ERC721TokenMinter(proxyAddr).initialize(tokenOwner, name, symbol, baseURI, royaltyReceiver, royaltyFeeNumerator); + emit ERC721TokenMinterDeployed(proxyAddr); + return proxyAddr; + } +} diff --git a/src/tokens/ERC721/presets/minter/IERC721TokenMinter.sol b/src/tokens/ERC721/presets/minter/IERC721TokenMinter.sol new file mode 100644 index 0000000..053168e --- /dev/null +++ b/src/tokens/ERC721/presets/minter/IERC721TokenMinter.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC721TokenMinterFunctions { + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param amount Amount of tokens to mint. + */ + function mint(address to, uint256 amount) external; + + /** + * Set name and symbol of token. + * @param tokenName Name of token. + * @param tokenSymbol Symbol of token. + */ + function setNameAndSymbol(string memory tokenName, string memory tokenSymbol) external; +} + +interface IERC721TokenMinterSignals { + /** + * Invalid initialization error. + */ + error InvalidInitialization(); +} + +interface IERC721TokenMinter is IERC721TokenMinterFunctions, IERC721TokenMinterSignals {} diff --git a/src/tokens/ERC721/presets/minter/IERC721TokenMinterFactory.sol b/src/tokens/ERC721/presets/minter/IERC721TokenMinterFactory.sol new file mode 100644 index 0000000..486f05a --- /dev/null +++ b/src/tokens/ERC721/presets/minter/IERC721TokenMinterFactory.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC721TokenMinterFactoryFunctions { + /** + * Creates an ERC-721 Token Minter proxy. + * @param proxyOwner The owner of the ERC-721 Token Minter proxy + * @param tokenOwner The owner of the ERC-721 Token Minter implementation + * @param name The name of the ERC-721 Token Minter proxy + * @param symbol The symbol of the ERC-721 Token Minter proxy + * @param baseURI The base URI of the ERC-721 Token Minter proxy + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @return proxyAddr The address of the ERC-721 Token Minter Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-721 Token Minter functions. + */ + function deploy( + address proxyOwner, + address tokenOwner, + string memory name, + string memory symbol, + string memory baseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + external + returns (address proxyAddr); +} + +interface IERC721TokenMinterFactorySignalss { + /** + * Event emitted when a new ERC-721 Token Minter proxy contract is deployed. + * @param proxyAddr The address of the deployed proxy. + */ + event ERC721TokenMinterDeployed(address proxyAddr); +} + +interface IERC721TokenMinterFactory is IERC721TokenMinterFactoryFunctions, IERC721TokenMinterFactorySignalss {} diff --git a/src/tokens/ERC721/presets/sale/ERC721Sale.sol b/src/tokens/ERC721/presets/sale/ERC721Sale.sol new file mode 100644 index 0000000..65d7ba4 --- /dev/null +++ b/src/tokens/ERC721/presets/sale/ERC721Sale.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {IERC721Sale} from "@0xsequence/contracts-library/tokens/ERC721/presets/sale/IERC721Sale.sol"; +import {ERC721Token} from "@0xsequence/contracts-library/tokens/ERC721/ERC721Token.sol"; +import { + WithdrawControlled, + AccessControl, + SafeERC20, + IERC20 +} from "@0xsequence/contracts-library/tokens/common/WithdrawControlled.sol"; +import {MerkleProofSingleUse} from "@0xsequence/contracts-library/tokens/common/MerkleProofSingleUse.sol"; + +/** + * An ERC-721 token contract with primary sale mechanisms. + */ +contract ERC721Sale is IERC721Sale, ERC721Token, WithdrawControlled, MerkleProofSingleUse { + bytes32 public constant MINT_ADMIN_ROLE = keccak256("MINT_ADMIN_ROLE"); + + bytes4 private constant _ERC20_TRANSFERFROM_SELECTOR = + bytes4(keccak256(bytes("transferFrom(address,address,uint256)"))); + + bool private _initialized; + + SaleDetails private _saleDetails; + + /** + * Initialize the contract. + * @param owner The owner of the contract + * @param tokenName Name of the token + * @param tokenSymbol Symbol of the token + * @param tokenBaseURI Base URI of the token + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @dev This should be called immediately after deployment. + */ + function initialize( + address owner, + string memory tokenName, + string memory tokenSymbol, + string memory tokenBaseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + public + virtual + { + if (_initialized) { + revert InvalidInitialization(); + } + + ERC721Token._initialize(owner, tokenName, tokenSymbol, tokenBaseURI); + _setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + + _setupRole(MINT_ADMIN_ROLE, owner); + _setupRole(WITHDRAW_ROLE, owner); + + _initialized = true; + } + + /** + * Checks if the current block.timestamp is out of the give timestamp range. + * @param _startTime Earliest acceptable timestamp (inclusive). + * @param _endTime Latest acceptable timestamp (exclusive). + * @dev A zero endTime value is always considered out of bounds. + */ + function _blockTimeOutOfBounds(uint256 _startTime, uint256 _endTime) private view returns (bool) { + // 0 end time indicates inactive sale. + return _endTime == 0 || block.timestamp < _startTime || block.timestamp >= _endTime; // solhint-disable-line not-rely-on-time + } + + /** + * Checks the sale is active and takes payment. + * @param _amount Amount of tokens to mint. + * @param _proof Merkle proof for allowlist minting. + */ + function _payForActiveMint(uint256 _amount, bytes32[] calldata _proof) private { + // Active sale test + if (_blockTimeOutOfBounds(_saleDetails.startTime, _saleDetails.endTime)) { + revert SaleInactive(); + } + requireMerkleProof(_saleDetails.merkleRoot, _proof, msg.sender); + + uint256 total = _saleDetails.cost * _amount; + address paymentToken = _saleDetails.paymentToken; + if (paymentToken == address(0)) { + // Paid in ETH + if (msg.value != total) { + revert InsufficientPayment(total, msg.value); + } + } else { + // Paid in ERC20 + SafeERC20.safeTransferFrom(IERC20(paymentToken), msg.sender, address(this), total); + } + } + + // + // Minting + // + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param amount Amount of tokens to mint. + * @param proof Merkle proof for allowlist minting. + * @notice Sale must be active for all tokens. + * @dev An empty proof is supplied when no proof is required. + */ + function mint(address to, uint256 amount, bytes32[] calldata proof) public payable { + uint256 currentSupply = totalSupply(); + uint256 supplyCap = _saleDetails.supplyCap; + if (supplyCap > 0 && currentSupply + amount > supplyCap) { + revert InsufficientSupply(currentSupply, amount, supplyCap); + } + _payForActiveMint(amount, proof); + _mint(to, amount); + } + + /** + * Mint tokens as admin. + * @param to Address to mint tokens to. + * @param amount Amount of tokens to mint. + * @notice Only callable by mint admin. + */ + function mintAdmin(address to, uint256 amount) public onlyRole(MINT_ADMIN_ROLE) { + _mint(to, amount); + } + + /** + * Set the sale details. + * @param supplyCap The maximum number of tokens that can be minted. 0 indicates unlimited supply. + * @param cost The amount of payment tokens to accept for each token minted. + * @param paymentToken The ERC20 token address to accept payment in. address(0) indicates ETH. + * @param startTime The start time of the sale. Tokens cannot be minted before this time. + * @param endTime The end time of the sale. Tokens cannot be minted after this time. + * @param merkleRoot The merkle root for allowlist minting. + * @dev A zero end time indicates an inactive sale. + */ + function setSaleDetails( + uint256 supplyCap, + uint256 cost, + address paymentToken, + uint64 startTime, + uint64 endTime, + bytes32 merkleRoot + ) + public + onlyRole(MINT_ADMIN_ROLE) + { + _saleDetails = SaleDetails(supplyCap, cost, paymentToken, startTime, endTime, merkleRoot); + emit SaleDetailsUpdated(supplyCap, cost, paymentToken, startTime, endTime, merkleRoot); + } + + // + // Admin + // + + /** + * Set name and symbol of token. + * @param tokenName Name of token. + * @param tokenSymbol Symbol of token. + */ + function setNameAndSymbol(string memory tokenName, string memory tokenSymbol) + external + onlyRole(METADATA_ADMIN_ROLE) + { + _tokenName = tokenName; + _tokenSymbol = tokenSymbol; + } + + // + // Views + // + + /** + * Get sale details. + * @return Sale details. + */ + function saleDetails() external view returns (SaleDetails memory) { + return _saleDetails; + } + + /** + * Check interface support. + * @param interfaceId Interface id + * @return True if supported + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override (ERC721Token, AccessControl) + returns (bool) + { + return interfaceId == type(IERC721Sale).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/src/tokens/ERC721/presets/sale/ERC721SaleFactory.sol b/src/tokens/ERC721/presets/sale/ERC721SaleFactory.sol new file mode 100644 index 0000000..39c1aa5 --- /dev/null +++ b/src/tokens/ERC721/presets/sale/ERC721SaleFactory.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {ERC721Sale} from "@0xsequence/contracts-library/tokens/ERC721/presets/sale/ERC721Sale.sol"; +import {IERC721SaleFactory} from "@0xsequence/contracts-library/tokens/ERC721/presets/sale/IERC721SaleFactory.sol"; +import {SequenceProxyFactory} from "@0xsequence/contracts-library/proxies/SequenceProxyFactory.sol"; + +/** + * Deployer of ERC-721 Sale proxies. + */ +contract ERC721SaleFactory is IERC721SaleFactory, SequenceProxyFactory { + /** + * Creates an ERC-721 Sale Factory. + * @param factoryOwner The owner of the ERC-721 Sale Factory + */ + constructor(address factoryOwner) { + ERC721Sale impl = new ERC721Sale(); + SequenceProxyFactory._initialize(address(impl), factoryOwner); + } + + /** + * Creates an ERC-721 Sale for given token contract + * @param proxyOwner The owner of the ERC-721 Sale proxy + * @param tokenOwner The owner of the ERC-721 Sale implementation + * @param name The name of the ERC-721 Sale token + * @param symbol The symbol of the ERC-721 Sale token + * @param baseURI The base URI of the ERC-721 Sale token + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @return proxyAddr The address of the ERC-721 Sale Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-721 Sale functions. + */ + function deploy( + address proxyOwner, + address tokenOwner, + string memory name, + string memory symbol, + string memory baseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + external + returns (address proxyAddr) + { + bytes32 salt = + keccak256(abi.encodePacked(tokenOwner, name, symbol, baseURI, royaltyReceiver, royaltyFeeNumerator)); + proxyAddr = _createProxy(salt, proxyOwner, ""); + ERC721Sale(proxyAddr).initialize(tokenOwner, name, symbol, baseURI, royaltyReceiver, royaltyFeeNumerator); + emit ERC721SaleDeployed(proxyAddr); + return proxyAddr; + } +} diff --git a/src/tokens/ERC721/presets/sale/IERC721Sale.sol b/src/tokens/ERC721/presets/sale/IERC721Sale.sol new file mode 100644 index 0000000..f538283 --- /dev/null +++ b/src/tokens/ERC721/presets/sale/IERC721Sale.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC721SaleFunctions { + + struct SaleDetails { + uint256 supplyCap; // 0 supply cap indicates unlimited supply + uint256 cost; + address paymentToken; // ERC20 token address for payment. address(0) indicated payment in ETH. + uint64 startTime; + uint64 endTime; // 0 end time indicates sale inactive + bytes32 merkleRoot; // Root of allowed addresses + } + + /** + * Mint tokens. + * @param to Address to mint tokens to. + * @param amount Amount of tokens to mint. + * @param proof Merkle proof for allowlist minting. + * @notice Sale must be active for all tokens. + * @dev An empty proof is supplied when no proof is required. + */ + function mint(address to, uint256 amount, bytes32[] memory proof) external payable; + + /** + * Set the sale details. + * @param supplyCap The maximum number of tokens that can be minted. 0 indicates unlimited supply. + * @param cost The amount of payment tokens to accept for each token minted. + * @param paymentToken The ERC20 token address to accept payment in. address(0) indicates ETH. + * @param startTime The start time of the sale. Tokens cannot be minted before this time. + * @param endTime The end time of the sale. Tokens cannot be minted after this time. + * @param merkleRoot The merkle root for allowlist minting. + */ + function setSaleDetails( + uint256 supplyCap, + uint256 cost, + address paymentToken, + uint64 startTime, + uint64 endTime, + bytes32 merkleRoot + ) external; + + /** + * Get sale details. + * @return Sale details. + */ + function saleDetails() external view returns (SaleDetails memory); + + /** + * Set name and symbol of token. + * @param tokenName Name of token. + * @param tokenSymbol Symbol of token. + */ + function setNameAndSymbol(string memory tokenName, string memory tokenSymbol) external; +} + +interface IERC721SaleSignals { + event SaleDetailsUpdated(uint256 supplyCap, uint256 cost, address paymentToken, uint64 startTime, uint64 endTime, bytes32 merkleRoot); + + /** + * Contract already initialized. + */ + error InvalidInitialization(); + + /** + * Sale is not active. + */ + error SaleInactive(); + + /** + * Insufficient supply. + * @param currentSupply Current supply. + * @param amount Amount to mint. + * @param maxSupply Maximum supply. + */ + error InsufficientSupply(uint256 currentSupply, uint256 amount, uint256 maxSupply); + + /** + * Insufficient tokens for payment. + * @param expected Expected amount of tokens. + * @param actual Actual amount of tokens. + */ + error InsufficientPayment(uint256 expected, uint256 actual); +} + +interface IERC721Sale is IERC721SaleFunctions, IERC721SaleSignals {} diff --git a/src/tokens/ERC721/presets/sale/IERC721SaleFactory.sol b/src/tokens/ERC721/presets/sale/IERC721SaleFactory.sol new file mode 100644 index 0000000..ac2933e --- /dev/null +++ b/src/tokens/ERC721/presets/sale/IERC721SaleFactory.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC721SaleFactoryFunctions { + /** + * Creates an ERC-721 Sale for given token contract + * @param proxyOwner The owner of the ERC-721 Sale proxy + * @param tokenOwner The owner of the ERC-721 Sale implementation + * @param name The name of the ERC-721 Sale token + * @param symbol The symbol of the ERC-721 Sale token + * @param baseURI The base URI of the ERC-721 Sale token + * @param royaltyReceiver Address of who should be sent the royalty payment + * @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @return proxyAddr The address of the ERC-721 Sale Proxy + * @dev As `proxyOwner` owns the proxy, it will be unable to call the ERC-721 Sale functions. + */ + function deploy( + address proxyOwner, + address tokenOwner, + string memory name, + string memory symbol, + string memory baseURI, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) + external + returns (address proxyAddr); +} + +interface IERC721SaleFactorySignals { + /** + * Event emitted when a new ERC-721 Sale proxy contract is deployed. + * @param proxyAddr The address of the deployed proxy. + */ + event ERC721SaleDeployed(address proxyAddr); +} + +interface IERC721SaleFactory is IERC721SaleFactoryFunctions, IERC721SaleFactorySignals {} diff --git a/src/tokens/common/ERC2981Controlled.sol b/src/tokens/common/ERC2981Controlled.sol new file mode 100644 index 0000000..972ec71 --- /dev/null +++ b/src/tokens/common/ERC2981Controlled.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {IERC2981Controlled} from "@0xsequence/contracts-library/tokens/common/IERC2981Controlled.sol"; +import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * An implementation of ERC-2981 that allows updates by roles. + */ +abstract contract ERC2981Controlled is ERC2981, AccessControl, IERC2981Controlled { + bytes32 public constant ROYALTY_ADMIN_ROLE = keccak256("ROYALTY_ADMIN_ROLE"); + + // + // Royalty + // + + /** + * Sets the royalty information that all ids in this contract will default to. + * @param receiver Address of who should be sent the royalty payment + * @param feeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + */ + function setDefaultRoyalty(address receiver, uint96 feeNumerator) external onlyRole(ROYALTY_ADMIN_ROLE) { + _setDefaultRoyalty(receiver, feeNumerator); + } + + /** + * Sets the royalty information that a given token id in this contract will use. + * @param tokenId The token id to set the royalty information for + * @param receiver Address of who should be sent the royalty payment + * @param feeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @notice This overrides the default royalty information for this token id + */ + function setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) + external + onlyRole(ROYALTY_ADMIN_ROLE) + { + _setTokenRoyalty(tokenId, receiver, feeNumerator); + } + + // + // Views + // + + /** + * Check interface support. + * @param interfaceId Interface id + * @return True if supported + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override (ERC2981, AccessControl) + returns (bool) + { + return ERC2981.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId) + || type(IERC2981Controlled).interfaceId == interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/src/tokens/common/IERC2981Controlled.sol b/src/tokens/common/IERC2981Controlled.sol new file mode 100644 index 0000000..2d9a076 --- /dev/null +++ b/src/tokens/common/IERC2981Controlled.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC2981ControlledFunctions { + /** + * Sets the royalty information that all ids in this contract will default to. + * @param receiver Address of who should be sent the royalty payment + * @param feeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + */ + function setDefaultRoyalty(address receiver, uint96 feeNumerator) external; + + /** + * Sets the royalty information that a given token id in this contract will use. + * @param tokenId The token id to set the royalty information for + * @param receiver Address of who should be sent the royalty payment + * @param feeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500) + * @notice This overrides the default royalty information for this token id + */ + function setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) external; +} + +interface IERC2981Controlled is IERC2981ControlledFunctions {} diff --git a/src/tokens/common/IMerkleProofSingleUse.sol b/src/tokens/common/IMerkleProofSingleUse.sol new file mode 100644 index 0000000..e7f64dd --- /dev/null +++ b/src/tokens/common/IMerkleProofSingleUse.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IMerkleProofSingleUseFunctions { + + /** + * Checks if the given merkle proof is valid. + * @param root Merkle root. + * @param proof Merkle proof. + * @param addr Address to check. + * @return True if the proof is valid and has not yet been used by {addr}. + */ + function checkMerkleProof(bytes32 root, bytes32[] calldata proof, address addr) external view returns (bool); +} + +interface IMerkleProofSingleUseSignals { + + /** + * Thrown when the merkle proof is invalid or has already been used. + * @param root Merkle root. + * @param proof Merkle proof. + * @param addr Address to check. + */ + error MerkleProofInvalid(bytes32 root, bytes32[] proof, address addr); + +} + +interface IMerkleProofSingleUse is IMerkleProofSingleUseFunctions, IMerkleProofSingleUseSignals {} diff --git a/src/tokens/common/IWithdrawControlled.sol b/src/tokens/common/IWithdrawControlled.sol new file mode 100644 index 0000000..7f9de7b --- /dev/null +++ b/src/tokens/common/IWithdrawControlled.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IWithdrawControlledFunctions { + + /** + * Withdraws ERC20 tokens owned by this contract. + * @param token The ERC20 token address. + * @param to Address to withdraw to. + * @param value Amount to withdraw. + */ + function withdrawERC20(address token, address to, uint256 value) external; + + /** + * Withdraws ETH owned by this sale contract. + * @param to Address to withdraw to. + * @param value Amount to withdraw. + */ + function withdrawETH(address to, uint256 value) external; +} + +interface IWithdrawControlledSignals { + + /** + * Withdraw failed error. + */ + error WithdrawFailed(); +} + +interface IWithdrawControlled is IWithdrawControlledFunctions, IWithdrawControlledSignals {} diff --git a/src/tokens/common/MerkleProofSingleUse.sol b/src/tokens/common/MerkleProofSingleUse.sol new file mode 100644 index 0000000..13473ff --- /dev/null +++ b/src/tokens/common/MerkleProofSingleUse.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {IMerkleProofSingleUse} from "@0xsequence/contracts-library/tokens/common/IMerkleProofSingleUse.sol"; +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +/** + * Require single use merkle proofs per address. + */ +abstract contract MerkleProofSingleUse is IMerkleProofSingleUse { + + // Stores proofs used by an address + mapping(address => mapping(bytes32 => bool)) private _proofUsed; + + /** + * Requires the given merkle proof to be valid. + * @param root Merkle root. + * @param proof Merkle proof. + * @param addr Address to check. + * @notice Fails when the proof is invalid or the proof has already been claimed by this address. + * @dev This function reverts on failure. + */ + function requireMerkleProof(bytes32 root, bytes32[] calldata proof, address addr) internal { + if (root != bytes32(0)) { + if (!checkMerkleProof(root, proof, addr)) { + revert MerkleProofInvalid(root, proof, addr); + } + _proofUsed[addr][root] = true; + } + } + + /** + * Checks if the given merkle proof is valid. + * @param root Merkle root. + * @param proof Merkle proof. + * @param addr Address to check. + * @return True if the proof is valid and has not yet been used by {addr}. + */ + function checkMerkleProof(bytes32 root, bytes32[] calldata proof, address addr) public view returns (bool) { + return !_proofUsed[addr][root] && MerkleProof.verify(proof, root, keccak256(abi.encodePacked(addr))); + } + +} diff --git a/src/tokens/common/README.md b/src/tokens/common/README.md new file mode 100644 index 0000000..0c739a4 --- /dev/null +++ b/src/tokens/common/README.md @@ -0,0 +1,85 @@ +# Common Token Functionality + +This section contains common contracts that can be used for additional functionality beyond the base token standards. + +## ERC2981Controlled + +The `ERC2981Controlled` contract is an implementation of the [ERC2981 token royalty standard](https://eips.ethereum.org/EIPS/eip-2981), which provides a standardized way to handle royalties in NFTs and SFTs. + +This contract allows the royalty information for the contract as a whole, or individual token IDs, to be updated by users with the `ROYALTY_ADMIN_ROLE`. + +### Functions + +* `setDefaultRoyalty(address receiver, uint96 feeNumerator)`: Sets the default royalty information for all token IDs in the contract. The `receiver` is the address that will receive royalty payments, and the `feeNumerator` is the royalty fee expressed in basis points (e.g., 15% would be 1500). This function is restricted to users with the `ROYALTY_ADMIN_ROLE`. +* `setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator)`: Sets the royalty information for a specific token ID, overriding the default royalty information for that token ID. The parameters are the same as in `setDefaultRoyalty`. This function is restricted to users with the `ROYALTY_ADMIN_ROLE`. + +### Usage + +To use this contract, it should be inherited by the main token contract. For example: + +```solidity +contract MyNFT is ERC721, ERC2981Controlled { + // ... +} +``` + +After that, the royalty information can be set and updated by users with the `ROYALTY_ADMIN_ROLE`. + +Alternatively, use the `ERC721Token` or `ERC1155Token` implementations which already extend this contract. + +### Dependencies + +The `ERC2981Controlled` contract depends on OpenZeppelin's `ERC2981` and `AccessControl` contracts. `ERC2981` provides the basic royalty-related functionality according to the standard, while `AccessControl` provides a flexible system of access control based on roles. + +## MerkleProofSingleUse + +The `MerkleProofSingleUse` contract provides a way to verify that a given value is included in a Merkle tree, and that it has not been used before. +This is useful for verifying that a given token ID has not been used before, for example in a claim process. + +### Functions + +* `checkMerkleProof(bytes32 root, bytes32[] calldata proof, address addr)`: An internal function that allows a contract to verify that a given value is included in a Merkle tree. The `root` is the Merkle root, the `proof` is the Merkle proof, and the `addr` is the value to verify. If the value is not included in the Merkle tree or if the proof has already been used, the function will return false. +* `requireMerkleProof(bytes32 root, bytes32[] calldata proof, address addr)`: An internal function that does the same as above, and also marks the proof has having been used by the address. + +### Usage + +To use this contract, it should be inherited by the main token contract and functions called as required. For example when minting in an NFT contract: + +```solidity +contract MyNFT is ERC721, MerkleProofSingleUse { + + function mint(address to, bytes32 root, bytes32[] calldata proof) public { + requireMerkleProof(root, proof, to); + _mint(to, tokenId); + } + + //... +} +``` + +### Dependencies + +The `MerkleProofSingleUse` contract depends on OpenZeppelin's `MerkleProof` contract. `MerkleProof` provides the basic Merkle proof verification functionality. + +## WithdrawControlled + +The `WithdrawControlled` contract provides a way to withdraw ETH and ERC20 tokens from a contract. This is useful for contracts that receive ETH or ERC20 tokens, and need to be able to withdraw them. + +### Functions + +* `withdrawETH(address payable to, uint256 value)`: Allows an address with the `WITHDRAW_ROLE` to withdraw ETH from the contract. The `to` parameter is the address to withdraw to, and the `value` is the amount to withdraw. +* `withdrawERC20(address token, address to, uint256 value)`: Allows an address with the `WITHDRAW_ROLE` to withdraw ERC20 tokens from the contract. The `token` parameter is the address of the ERC20 token to withdraw, the `to` parameter is the address to withdraw to, and the `value` is the amount to withdraw. + +### Usage + +To use this contract, it should be inherited by the main token contract. For example: + +```solidity +contract MyNFT is ERC721, WithdrawControlled { + // ... +} +``` + +### Dependencies + +The `WithdrawControlled` contract depends on OpenZeppelin's `AccessControl` contract. `AccessControl` provides a flexible system of access control based on roles. diff --git a/src/tokens/common/WithdrawControlled.sol b/src/tokens/common/WithdrawControlled.sol new file mode 100644 index 0000000..372cc48 --- /dev/null +++ b/src/tokens/common/WithdrawControlled.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {IWithdrawControlled} from "@0xsequence/contracts-library/tokens/common/IWithdrawControlled.sol"; +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * An abstract contract that allows ETH and ERC20 tokens stored in the contract to be withdrawn. + */ +abstract contract WithdrawControlled is AccessControl, IWithdrawControlled { + bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE"); + + // + // Withdraw + // + + /** + * Withdraws ERC20 tokens owned by this contract. + * @param token The ERC20 token address. + * @param to Address to withdraw to. + * @param value Amount to withdraw. + * @notice Only callable by an address with the withdraw role. + */ + function withdrawERC20(address token, address to, uint256 value) public onlyRole(WITHDRAW_ROLE) { + SafeERC20.safeTransfer(IERC20(token), to, value); + } + + /** + * Withdraws ETH owned by this sale contract. + * @param to Address to withdraw to. + * @param value Amount to withdraw. + * @notice Only callable by an address with the withdraw role. + */ + function withdrawETH(address to, uint256 value) public onlyRole(WITHDRAW_ROLE) { + (bool success,) = to.call{value: value}(""); + if (!success) { + revert WithdrawFailed(); + } + } +} diff --git a/src/utils/StorageSlot.sol b/src/utils/StorageSlot.sol new file mode 100644 index 0000000..b45b9fb --- /dev/null +++ b/src/utils/StorageSlot.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function _getAddressSlot(bytes32 _slot) internal pure returns (AddressSlot storage r) { + assembly { // solhint-disable-line no-inline-assembly + r.slot := _slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function _getBooleanSlot(bytes32 _slot) internal pure returns (BooleanSlot storage r) { + assembly { // solhint-disable-line no-inline-assembly + r.slot := _slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function _getBytes32Slot(bytes32 _slot) internal pure returns (Bytes32Slot storage r) { + assembly { // solhint-disable-line no-inline-assembly + r.slot := _slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function _getUint256Slot(bytes32 _slot) internal pure returns (Uint256Slot storage r) { + assembly { // solhint-disable-line no-inline-assembly + r.slot := _slot + } + } +} diff --git a/test/proxies/SequenceProxyFactory.t.sol b/test/proxies/SequenceProxyFactory.t.sol new file mode 100644 index 0000000..8178544 --- /dev/null +++ b/test/proxies/SequenceProxyFactory.t.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {SequenceProxyFactory} from "src/proxies/SequenceProxyFactory.sol"; +import {ITransparentUpgradeableProxy} from "src/proxies/openzeppelin/TransparentUpgradeableProxy.sol"; + +contract MockImplementationV1 { + function getValue() public pure virtual returns (uint256) { + return 1; + } +} + +contract MockImplementationV2 is MockImplementationV1 { + function getValue() public pure virtual override returns (uint256) { + return 2; + } +} + +contract MockImplementationV3 is MockImplementationV1 { + function getValue() public pure virtual override returns (uint256) { + return 3; + } +} + +contract PublicSequenceProxyFactory is SequenceProxyFactory { + constructor(address implementation, address factoryOwner) { + _initialize(implementation, factoryOwner); + } + + function createProxy(bytes32 salt, address proxyOwner, bytes memory data) external returns (address proxyAddress) { + return _createProxy(salt, proxyOwner, data); + } + + function computeProxyAddress(bytes32 salt, address proxyOwner, bytes memory data) external view returns (address) { + return _computeProxyAddress(salt, proxyOwner, data); + } +} + +contract SequenceProxyFactoryTest is Test { + + PublicSequenceProxyFactory private factory; + address private proxyOwner; + address private impl1; + address private impl2; + address private impl3; + + function setUp() public { + proxyOwner = makeAddr("proxyOwner"); + impl1 = address(new MockImplementationV1()); + impl2 = address(new MockImplementationV2()); + impl3 = address(new MockImplementationV3()); + factory = new PublicSequenceProxyFactory(impl1, address(this)); + } + + function testDeployProxy() public { + address proxy = factory.createProxy(bytes32(""), proxyOwner, bytes("")); + + assertTrue(proxy != address(0)); + uint256 value = MockImplementationV1(proxy).getValue(); + assertEq(value, 1); + } + + function testDeployProxyAfterUpgrade() public { + factory.upgradeBeacon(impl2); + + address proxy = factory.createProxy(bytes32(""), proxyOwner, bytes("")); + + assertTrue(proxy != address(0)); + uint256 value = MockImplementationV1(proxy).getValue(); + assertEq(value, 2); + } + + function testUpgradeAfterDeploy() public { + address proxy = factory.createProxy(bytes32(""), proxyOwner, bytes("")); + assertTrue(proxy != address(0)); + + factory.upgradeBeacon(impl2); + uint256 value = MockImplementationV1(proxy).getValue(); + assertEq(value, 2); + } + + function testProxyOwnerUpgrade() public { + address proxy = factory.createProxy(bytes32(""), proxyOwner, bytes("")); + assertTrue(proxy != address(0)); + + vm.prank(proxyOwner); + ITransparentUpgradeableProxy(payable(proxy)).upgradeTo(impl2); + + uint256 value = MockImplementationV1(proxy).getValue(); + assertEq(value, 2); + } + + function testProxyOwnerUpgradeUnaffectedByBeaconUpgrades() public { + address proxy = factory.createProxy(bytes32(""), proxyOwner, bytes("")); + assertTrue(proxy != address(0)); + + vm.prank(proxyOwner); + ITransparentUpgradeableProxy(payable(proxy)).upgradeTo(impl3); + + // Upgrade beacon + factory.upgradeBeacon(impl2); + + uint256 value = MockImplementationV1(proxy).getValue(); + assertEq(value, 3); + } + + function testAddressCompute() public { + address expected = factory.createProxy(bytes32(""), proxyOwner, bytes("")); + address actual = factory.computeProxyAddress(bytes32(""), proxyOwner, bytes("")); + assertEq(actual, expected); + } + + function testDuplicateDeploysFail() public { + address proxy = factory.createProxy(bytes32(""), proxyOwner, bytes("")); + assertTrue(proxy != address(0)); + + vm.expectRevert("Create2: Failed on deploy"); + factory.createProxy(bytes32(""), proxyOwner, bytes("")); + } + +} diff --git a/test/tokens/ERC1155/presets/ERC1155Sale.t.sol b/test/tokens/ERC1155/presets/ERC1155Sale.t.sol new file mode 100644 index 0000000..38342c3 --- /dev/null +++ b/test/tokens/ERC1155/presets/ERC1155Sale.t.sol @@ -0,0 +1,673 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {IERC1155SaleSignals} from "src/tokens/ERC1155/presets/sale/IERC1155Sale.sol"; +import {ERC1155Sale} from "src/tokens/ERC1155/presets/sale/ERC1155Sale.sol"; +import {ERC1155SaleFactory} from "src/tokens/ERC1155/presets/sale/ERC1155SaleFactory.sol"; +import {IERC1155SupplySignals} from "src/tokens/ERC1155/extensions/supply/IERC1155Supply.sol"; + +import {Merkle} from "murky/Merkle.sol"; +import {ERC20Mock} from "@0xsequence/erc20-meta-token/contracts/mocks/ERC20Mock.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {TestHelper} from "test/tokens/TestHelper.sol"; +import {IMerkleProofSingleUseSignals} from "@0xsequence/contracts-library/tokens/common/IMerkleProofSingleUse.sol"; + +// Interfaces +import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; +import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {IERC1155Metadata} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155Metadata.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; + +// solhint-disable no-rely-on-time + +contract ERC1155SaleTest is Test, Merkle, IERC1155SaleSignals, IERC1155SupplySignals, IMerkleProofSingleUseSignals { + // Redeclare events + event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _amounts + ); + + ERC1155Sale private token; + ERC20Mock private erc20; + uint256 private perTokenCost = 0.02 ether; + + address private proxyOwner; + + address private constant ALLOWLIST_ADDR = 0xFA4eE536359087Fba7BD3248EE09e8Cc8347F8Ed; + + function setUp() public { + proxyOwner = makeAddr("proxyOwner"); + + token = new ERC1155Sale(); + token.initialize(address(this), "test", "ipfs://", address(this), 0); + + vm.deal(address(this), 1e6 ether); + } + + function setUpFromFactory() public { + ERC1155SaleFactory factory = new ERC1155SaleFactory(address(this)); + token = ERC1155Sale(factory.deploy(proxyOwner, address(this), "test", "ipfs://", address(this), 0)); + } + + function testSupportsInterface() public { + assertTrue(token.supportsInterface(type(IERC165).interfaceId)); + assertTrue(token.supportsInterface(type(IERC1155).interfaceId)); + assertTrue(token.supportsInterface(type(IERC1155Metadata).interfaceId)); + assertTrue(token.supportsInterface(type(IAccessControl).interfaceId)); + } + + // + // Minting + // + + // Minting denied when no sale active. + function testMintInactiveFail(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId)); + token.mint{value: amount * perTokenCost}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + } + + // Minting denied when sale is active but not for the token. + function testMintInactiveSingleFail(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + withTokenSaleActive(tokenId + 1) + { + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId)); + token.mint{value: amount * perTokenCost}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + } + + // Minting denied when token sale is expired. + function testMintExpiredSingleFail( + bool useFactory, + address mintTo, + uint256 tokenId, + uint256 amount, + uint64 startTime, + uint64 endTime + ) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + if (startTime > endTime) { + uint64 temp = startTime; + startTime = endTime; + endTime = temp; + } + if (block.timestamp >= startTime && block.timestamp <= endTime) { + vm.warp(uint256(endTime) + 1); + } + token.setTokenSaleDetails(tokenId, perTokenCost, 0, startTime, endTime, ""); + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId)); + token.mint{value: amount * perTokenCost}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + } + + // Minting denied when global sale is expired. + function testMintExpiredGlobalFail( + bool useFactory, + address mintTo, + uint256 tokenId, + uint256 amount, + uint64 startTime, + uint64 endTime + ) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + if (startTime > endTime) { + uint64 temp = startTime; + startTime = endTime; + endTime = temp; + } + if (block.timestamp >= startTime && block.timestamp <= endTime) { + vm.warp(uint256(endTime) + 1); + } + token.setGlobalSaleDetails(perTokenCost, 0, address(0), startTime, endTime, ""); + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId)); + token.mint{value: amount * perTokenCost}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + } + + // Minting denied when sale is active but not for all tokens in the group. + function testMintInactiveInGroupFail(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + withTokenSaleActive(tokenId) + { + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = tokenId; + tokenIds[1] = tokenId + 1; + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount; + amounts[1] = amount; + + vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId + 1)); + token.mint{value: amount * perTokenCost * 2}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + } + + // Minting denied when global supply exceeded. + function testMintGlobalSupplyExceeded( + bool useFactory, + address mintTo, + uint256 tokenId, + uint256 amount, + uint256 supplyCap + ) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + if (supplyCap == 0 || supplyCap > 20) { + supplyCap = 1; + } + if (amount <= supplyCap) { + amount = supplyCap + 1; + } + token.setGlobalSaleDetails( + perTokenCost, supplyCap, address(0), uint64(block.timestamp), uint64(block.timestamp + 1), "" + ); + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + vm.expectRevert(abi.encodeWithSelector(InsufficientSupply.selector, 0, amount, supplyCap)); + token.mint{value: amount * perTokenCost}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + } + + // Minting denied when token supply exceeded. + function testMintTokenSupplyExceeded( + bool useFactory, + address mintTo, + uint256 tokenId, + uint256 amount, + uint256 supplyCap + ) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + if (supplyCap == 0 || supplyCap > 20) { + supplyCap = 1; + } + if (amount <= supplyCap) { + amount = supplyCap + 1; + } + token.setTokenSaleDetails( + tokenId, perTokenCost, supplyCap, uint64(block.timestamp), uint64(block.timestamp + 1), "" + ); + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + vm.expectRevert(abi.encodeWithSelector(InsufficientSupply.selector, 0, amount, supplyCap)); + token.mint{value: amount * perTokenCost}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + } + + // Minting allowed when sale is active globally. + function testMintGlobalSuccess(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + withGlobalSaleActive + { + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + uint256 count = token.balanceOf(mintTo, tokenId); + vm.expectEmit(true, true, true, true); + emit TransferBatch(address(this), address(0), mintTo, tokenIds, amounts); + token.mint{value: amount * perTokenCost}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + } + + // Minting allowed when sale is active for the token. + function testMintSingleSuccess(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + withTokenSaleActive(tokenId) + { + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + uint256 count = token.balanceOf(mintTo, tokenId); + vm.expectEmit(true, true, true, true); + emit TransferBatch(address(this), address(0), mintTo, tokenIds, amounts); + token.mint{value: amount * perTokenCost}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + } + + // Minting allowed when sale is active for both tokens individually. + function testMintGroupSuccess(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + setTokenSaleActive(tokenId); + setTokenSaleActive(tokenId + 1); + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = tokenId; + tokenIds[1] = tokenId + 1; + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount; + amounts[1] = amount; + + uint256 count = token.balanceOf(mintTo, tokenId); + uint256 count2 = token.balanceOf(mintTo, tokenId + 1); + vm.expectEmit(true, true, true, true); + emit TransferBatch(address(this), address(0), mintTo, tokenIds, amounts); + token.mint{value: amount * perTokenCost * 2}(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + assertEq(count2 + amount, token.balanceOf(mintTo, tokenId + 1)); + } + + // Minting allowed when global sale is free. + function testFreeGlobalMint(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + token.setGlobalSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), ""); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + uint256 count = token.balanceOf(mintTo, tokenId); + vm.expectEmit(true, true, true, true); + emit TransferBatch(address(this), address(0), mintTo, tokenIds, amounts); + token.mint(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + } + + // Minting allowed when token sale is free and global is not. + function testFreeTokenMint(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + withGlobalSaleActive + { + token.setTokenSaleDetails(tokenId, 0, 0, uint64(block.timestamp - 1), uint64(block.timestamp + 1), ""); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + uint256 count = token.balanceOf(mintTo, tokenId); + vm.expectEmit(true, true, true, true); + emit TransferBatch(address(this), address(0), mintTo, tokenIds, amounts); + token.mint(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + } + + // Minting allowed when mint charged with ERC20. + function testERC20Mint(bool useFactory, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + withERC20 + { + token.setGlobalSaleDetails( + perTokenCost, 0, address(erc20), uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" + ); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256 cost = amount * perTokenCost; + + uint256 balanace = erc20.balanceOf(address(this)); + uint256 count = token.balanceOf(mintTo, tokenId); + vm.expectEmit(true, true, true, true); + emit TransferBatch(address(this), address(0), mintTo, tokenIds, amounts); + token.mint(mintTo, tokenIds, amounts, "", TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + assertEq(balanace - cost, erc20.balanceOf(address(this))); + assertEq(cost, erc20.balanceOf(address(token))); + } + + // Minting with merkle success. + function testMerkleSuccess(address[] memory allowlist, uint256 senderIndex, uint256 tokenId, bool globalActive) + public + { + // Construct a merkle tree with the allowlist. + vm.assume(allowlist.length > 1); + vm.assume(senderIndex < allowlist.length); + bytes32[] memory addrs = new bytes32[](allowlist.length); + for (uint256 i = 0; i < allowlist.length; i++) { + addrs[i] = keccak256(abi.encodePacked(allowlist[i])); + } + bytes32 root = getRoot(addrs); + if (globalActive) { + token.setGlobalSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + } else { + token.setTokenSaleDetails(tokenId, 0, 0, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + } + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(uint256(1)); + bytes32[] memory proof = getProof(addrs, senderIndex); + + address sender = allowlist[senderIndex]; + vm.prank(sender); + token.mint(sender, tokenIds, amounts, "", proof); + + assertEq(1, token.balanceOf(sender, tokenId)); + } + + // Minting with merkle reuse fail. + function testMerkleReuseFail(address[] memory allowlist, uint256 senderIndex, uint256 tokenId, bool globalActive) + public + { + // Copy of testMerkleSuccess + vm.assume(allowlist.length > 1); + vm.assume(senderIndex < allowlist.length); + bytes32[] memory addrs = new bytes32[](allowlist.length); + for (uint256 i = 0; i < allowlist.length; i++) { + addrs[i] = keccak256(abi.encodePacked(allowlist[i])); + } + bytes32 root = getRoot(addrs); + if (globalActive) { + token.setGlobalSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + } else { + token.setTokenSaleDetails(tokenId, 0, 0, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + } + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(uint256(1)); + bytes32[] memory proof = getProof(addrs, senderIndex); + + address sender = allowlist[senderIndex]; + vm.prank(sender); + token.mint(sender, tokenIds, amounts, "", proof); + + assertEq(1, token.balanceOf(sender, tokenId)); + // End copy + + vm.expectRevert(abi.encodeWithSelector(MerkleProofInvalid.selector, root, proof, sender)); + vm.prank(sender); + token.mint(sender, tokenIds, amounts, "", proof); + } + + // Minting with merkle fail no proof. + function testMerkleFailNoProof(address[] memory allowlist, address sender, uint256 tokenId, bool globalActive) + public + { + // Construct a merkle tree with the allowlist. + vm.assume(allowlist.length > 1); + bytes32[] memory addrs = new bytes32[](allowlist.length); + for (uint256 i = 0; i < allowlist.length; i++) { + vm.assume(sender != allowlist[i]); + addrs[i] = keccak256(abi.encodePacked(allowlist[i])); + } + bytes32 root = getRoot(addrs); + if (globalActive) { + token.setGlobalSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + } else { + token.setTokenSaleDetails(tokenId, 0, 0, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + } + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(uint256(1)); + bytes32[] memory proof = TestHelper.blankProof(); + + vm.expectRevert(abi.encodeWithSelector(MerkleProofInvalid.selector, root, proof, sender)); + vm.prank(sender); + token.mint(sender, tokenIds, amounts, "", TestHelper.blankProof()); + } + + // Minting with merkle fail bad proof. + function testMerkleFailBadProof(address[] memory allowlist, address sender, uint256 tokenId, bool globalActive) + public + { + // Construct a merkle tree with the allowlist. + vm.assume(allowlist.length > 1); + bytes32[] memory addrs = new bytes32[](allowlist.length); + for (uint256 i = 0; i < allowlist.length; i++) { + vm.assume(sender != allowlist[i]); + addrs[i] = keccak256(abi.encodePacked(allowlist[i])); + } + bytes32 root = getRoot(addrs); + if (globalActive) { + token.setGlobalSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + } else { + token.setTokenSaleDetails(tokenId, 0, 0, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + } + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(uint256(1)); + bytes32[] memory proof = getProof(addrs, 1); // Wrong sender + + vm.expectRevert(abi.encodeWithSelector(MerkleProofInvalid.selector, root, proof, sender)); + vm.prank(sender); + token.mint(sender, tokenIds, amounts, "", proof); + } + + // + // Admin minting + // + + // Admin minting denied when not admin. + function testMintAdminFail(bool useFactory, address minter, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + vm.assume(minter != address(this)); + vm.assume(minter != address(0)); + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(minter), + " is missing role ", + Strings.toHexString(uint256(token.MINT_ADMIN_ROLE()), 32) + ) + ); + vm.prank(minter); + token.mintAdmin(mintTo, tokenIds, amounts, ""); + } + + // Minting as admin success. + function testMintAdminSuccess(bool useFactory, address minter, address mintTo, uint256 tokenId, uint256 amount) + public + assumeSafe(mintTo, tokenId, amount) + withFactory(useFactory) + { + token.grantRole(token.MINT_ADMIN_ROLE(), minter); + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + + uint256 count = token.balanceOf(mintTo, tokenId); + token.mintAdmin(mintTo, tokenIds, amounts, ""); + assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + } + + // + // Royalty + // + + // Token royalty fails if the caller doesn't have the ROYALTY_ADMIN_ROLE + function testSetTokenRoyaltyFail(uint256 _tokenId, address _receiver, uint96 _feeNumerator) public { + token.revokeRole(token.ROYALTY_ADMIN_ROLE(), address(this)); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(address(this)), + " is missing role ", + Strings.toHexString(uint256(token.ROYALTY_ADMIN_ROLE()), 32) + ) + ); + token.setTokenRoyalty(_tokenId, _receiver, _feeNumerator); + } + + // Default royalty fails if the caller doesn't have the ROYALTY_ADMIN_ROLE + function testSetDefaultRoyaltyFail(address _receiver, uint96 _feeNumerator) public { + token.revokeRole(token.ROYALTY_ADMIN_ROLE(), address(this)); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(address(this)), + " is missing role ", + Strings.toHexString(uint256(token.ROYALTY_ADMIN_ROLE()), 32) + ) + ); + token.setDefaultRoyalty(_receiver, _feeNumerator); + } + + // Royalty calculation for token with custom royalty information + function testRoyaltyInfoCustom(uint256 _tokenId, uint256 _salePrice, address _receiver, uint96 _feeNumerator) + public + { + vm.assume(_receiver != address(0)); + vm.assume(_feeNumerator < 10000); + vm.assume(_salePrice < 10000 ether); + + token.setTokenRoyalty(_tokenId, _receiver, _feeNumerator); + uint256 expectedRoyaltyAmount = (_salePrice * _feeNumerator) / 10000; + + (address receiver, uint256 royaltyAmount) = token.royaltyInfo(_tokenId, _salePrice); + + assertEq(_receiver, receiver); + assertEq(expectedRoyaltyAmount, royaltyAmount); + } + + // Test royalty calculation for token with default royalty information + function testRoyaltyInfoDefault(uint256 _tokenId, uint256 _salePrice, address _receiver, uint96 _feeNumerator) + public + { + vm.assume(_receiver != address(0)); + vm.assume(_feeNumerator < 10000); + vm.assume(_salePrice < 10000 ether); + + token.setDefaultRoyalty(_receiver, _feeNumerator); + uint256 expectedRoyaltyAmount = (_salePrice * _feeNumerator) / 10000; + + (address receiver, uint256 royaltyAmount) = token.royaltyInfo(_tokenId, _salePrice); + + assertEq(_receiver, receiver); + assertEq(expectedRoyaltyAmount, royaltyAmount); + } + + // + // Withdraw + // + + // Withdraw fails if the caller doesn't have the WITHDRAW_ROLE + function testWithdrawFail(bool useFactory, address withdrawTo, uint256 amount) public withFactory(useFactory) { + token.revokeRole(token.WITHDRAW_ROLE(), address(this)); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(address(this)), + " is missing role ", + Strings.toHexString(uint256(token.WITHDRAW_ROLE()), 32) + ) + ); + token.withdrawETH(withdrawTo, amount); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(address(this)), + " is missing role ", + Strings.toHexString(uint256(token.WITHDRAW_ROLE()), 32) + ) + ); + token.withdrawERC20(address(erc20), withdrawTo, amount); + } + + // Withdraw success ETH + function testWithdrawETH(bool useFactory, address withdrawTo, uint256 tokenId, uint256 amount) + public + withFactory(useFactory) + { + vm.assume(uint160(withdrawTo) > 16); + testMintSingleSuccess(false, withdrawTo, tokenId, amount); + + uint256 tokenBalance = address(token).balance; + uint256 balance = withdrawTo.balance; + token.withdrawETH(withdrawTo, tokenBalance); + assertEq(tokenBalance + balance, withdrawTo.balance); + } + + // Withdraw success ERC20 + function testWithdrawERC20(bool useFactory, address withdrawTo, uint256 tokenId, uint256 amount) + public + assumeSafe(withdrawTo, tokenId, amount) + withFactory(useFactory) + { + testERC20Mint(false, withdrawTo, tokenId, amount); + + uint256 tokenBalance = erc20.balanceOf(address(token)); + uint256 balance = erc20.balanceOf(withdrawTo); + token.withdrawERC20(address(erc20), withdrawTo, tokenBalance); + assertEq(tokenBalance + balance, erc20.balanceOf(withdrawTo)); + } + + // + // Helpers + // + modifier withFactory(bool useFactory) { + if (useFactory) { + setUpFromFactory(); + } + _; + } + + modifier assumeSafe(address nonContract, uint256 tokenId, uint256 amount) { + vm.assume(uint160(nonContract) > 16); + vm.assume(nonContract != proxyOwner); + vm.assume(nonContract.code.length == 0); + vm.assume(tokenId < 100); + vm.assume(amount > 0 && amount < 20); + _; + } + + // Create ERC20. Give this contract 1000 ERC20 tokens. Approve token to spend 100 ERC20 tokens. + modifier withERC20() { + erc20 = new ERC20Mock(); + erc20.mockMint(address(this), 1000 ether); + erc20.approve(address(token), 1000 ether); + _; + } + + modifier withGlobalSaleActive() { + token.setGlobalSaleDetails( + perTokenCost, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" + ); + _; + } + + modifier withTokenSaleActive(uint256 tokenId) { + setTokenSaleActive(tokenId); + _; + } + + function setTokenSaleActive(uint256 tokenId) private { + token.setTokenSaleDetails( + tokenId, perTokenCost, 0, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" + ); + } +} diff --git a/test/tokens/ERC1155/presets/ERC1155TokenMinter.t.sol b/test/tokens/ERC1155/presets/ERC1155TokenMinter.t.sol new file mode 100644 index 0000000..c2d72d8 --- /dev/null +++ b/test/tokens/ERC1155/presets/ERC1155TokenMinter.t.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {ERC1155TokenMinter} from "src/tokens/ERC1155/presets/minter/ERC1155TokenMinter.sol"; +import {IERC1155TokenMinterSignals} from "src/tokens/ERC1155/presets/minter/IERC1155TokenMinter.sol"; +import {ERC1155TokenMinterFactory} from "src/tokens/ERC1155/presets/minter/ERC1155TokenMinterFactory.sol"; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +// Interfaces +import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; +import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {IERC1155Metadata} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155Metadata.sol"; + +contract ERC1155TokenMinterTest is Test, IERC1155TokenMinterSignals { + // Redeclare events + event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _amount + ); + event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _amounts + ); + + ERC1155TokenMinter private token; + + address private proxyOwner; + address private owner; + + function setUp() public { + owner = makeAddr("owner"); + proxyOwner = makeAddr("proxyOwner"); + + vm.deal(address(this), 100 ether); + vm.deal(owner, 100 ether); + + ERC1155TokenMinterFactory factory = new ERC1155TokenMinterFactory(address(this)); + token = ERC1155TokenMinter(factory.deploy(proxyOwner, owner, "name", "baseURI", address(this), 0)); + } + + function testReinitializeFails() public { + vm.expectRevert(InvalidInitialization.selector); + token.initialize(owner, "name", "baseURI", address(this), 0); + } + + function testSupportsInterface() public { + assertTrue(token.supportsInterface(type(IERC165).interfaceId)); + assertTrue(token.supportsInterface(type(IERC1155).interfaceId)); + assertTrue(token.supportsInterface(type(IERC1155Metadata).interfaceId)); + } + + function testOwnerHasRoles() public { + assertTrue(token.hasRole(token.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(token.hasRole(token.METADATA_ADMIN_ROLE(), owner)); + assertTrue(token.hasRole(token.MINTER_ROLE(), owner)); + assertTrue(token.hasRole(token.ROYALTY_ADMIN_ROLE(), owner)); + } + + // + // Minting + // + function testMintInvalidRole(address caller) public { + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.MINTER_ROLE()), 32) + ) + ); + vm.prank(caller); + token.mint(caller, 1, 1, ""); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1; + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.MINTER_ROLE()), 32) + ) + ); + vm.prank(caller); + token.batchMint(caller, tokenIds, amounts, ""); + } + + function testMintOwner(uint256 tokenId, uint256 amount) public { + vm.assume(amount > 0); + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferSingle(owner, address(0), owner, tokenId, amount); + vm.prank(owner); + token.mint(owner, tokenId, amount, ""); + + assertEq(token.balanceOf(owner, tokenId), amount); + } + + function testBatchMintOwner(uint256[] memory tokenIds, uint256[] memory amounts) public { + tokenIds = boundArrayLength(tokenIds, 10); + amounts = boundArrayLength(amounts, 10); + vm.assume(tokenIds.length == amounts.length); + for (uint256 i; i < amounts.length; i++) { + vm.assume(amounts[i] > 0); + } + // Unique ids + for (uint256 i; i < tokenIds.length; i++) { + for (uint256 j; j < tokenIds.length; j++) { + if (i != j) { + vm.assume(tokenIds[i] != tokenIds[j]); + } + } + } + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(owner, address(0), owner, tokenIds, amounts); + vm.prank(owner); + token.batchMint(owner, tokenIds, amounts, ""); + } + + function testMintWithRole(address minter, uint256 tokenId, uint256 amount) public { + vm.assume(minter != owner); + vm.assume(minter != proxyOwner); + vm.assume(minter != address(0)); + // Give role + vm.startPrank(owner); + token.grantRole(token.MINTER_ROLE(), minter); + vm.stopPrank(); + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferSingle(minter, address(0), owner, tokenId, amount); + + vm.prank(minter); + token.mint(owner, tokenId, amount, ""); + + assertEq(token.balanceOf(owner, tokenId), amount); + } + + function testBatchMintWithRole(address minter, uint256[] memory tokenIds, uint256[] memory amounts) public { + vm.assume(minter != owner); + vm.assume(minter != proxyOwner); + vm.assume(minter != address(0)); + tokenIds = boundArrayLength(tokenIds, 10); + amounts = boundArrayLength(amounts, 10); + vm.assume(tokenIds.length == amounts.length); + for (uint256 i; i < amounts.length; i++) { + vm.assume(amounts[i] > 0); + } + // Unique ids + for (uint256 i; i < tokenIds.length; i++) { + for (uint256 j; j < tokenIds.length; j++) { + if (i != j) { + vm.assume(tokenIds[i] != tokenIds[j]); + } + } + } + + // Give role + vm.startPrank(owner); + token.grantRole(token.MINTER_ROLE(), minter); + vm.stopPrank(); + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(minter, address(0), owner, tokenIds, amounts); + vm.prank(minter); + token.batchMint(owner, tokenIds, amounts, ""); + } + + // + // Metadata + // + function testMetadataOwner() public { + vm.prank(owner); + token.setBaseMetadataURI("ipfs://newURI/"); + + assertEq(token.uri(0), "ipfs://newURI/0.json"); + assertEq(token.uri(1), "ipfs://newURI/1.json"); + } + + function testMetadataInvalid(address caller) public { + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.METADATA_ADMIN_ROLE()), 32) + ) + ); + vm.prank(caller); + token.setBaseMetadataURI("ipfs://newURI/"); + } + + function testMetadataWithRole(address caller) public { + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.assume(caller != address(0)); + // Give role + vm.startPrank(owner); + token.grantRole(token.METADATA_ADMIN_ROLE(), caller); + vm.stopPrank(); + + vm.prank(caller); + token.setBaseMetadataURI("ipfs://newURI/"); + } + + // + // Royalty + // + function testDefaultRoyalty(address receiver, uint96 feeNumerator, uint256 salePrice) public { + vm.assume(feeNumerator <= 10000); + vm.assume(receiver != address(0)); + vm.assume(salePrice < type(uint128).max); // Buffer for overflow + + vm.prank(owner); + token.setDefaultRoyalty(receiver, feeNumerator); + + (address receiver_, uint256 amount) = token.royaltyInfo(1, salePrice); + assertEq(receiver_, receiver); + assertEq(amount, salePrice * feeNumerator / 10000); + } + + function testTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator, uint256 salePrice) public { + vm.assume(feeNumerator <= 10000); + vm.assume(receiver != address(0)); + vm.assume(tokenId != 69); // Other token id for default validation + vm.assume(salePrice < type(uint128).max); // Buffer for overflow + + vm.prank(owner); + token.setTokenRoyalty(tokenId, receiver, feeNumerator); + + (address receiver_, uint256 amount) = token.royaltyInfo(tokenId, salePrice); + assertEq(receiver_, receiver); + assertEq(amount, salePrice * feeNumerator / 10000); + + (receiver_, amount) = token.royaltyInfo(69, salePrice); + assertEq(receiver_, address(this)); + assertEq(amount, 0); + } + + function testRoyaltyWithRole( + address caller, + uint256 tokenId, + address receiver, + uint96 feeNumerator, + uint256 salePrice + ) + public + { + vm.assume(feeNumerator <= 10000); + vm.assume(receiver != address(0)); + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.assume(salePrice < type(uint128).max); // Buffer for overflow + + vm.startPrank(owner); + token.grantRole(token.ROYALTY_ADMIN_ROLE(), caller); + vm.stopPrank(); + + vm.prank(caller); + token.setDefaultRoyalty(receiver, feeNumerator); + + (address receiver_, uint256 amount) = token.royaltyInfo(1, salePrice); + assertEq(receiver_, receiver); + assertEq(amount, salePrice * feeNumerator / 10000); + + vm.prank(caller); + token.setTokenRoyalty(tokenId, receiver, feeNumerator); + + (receiver_, amount) = token.royaltyInfo(tokenId, salePrice); + assertEq(receiver_, receiver); + assertEq(amount, salePrice * feeNumerator / 10000); + } + + function testRoyaltyInvalidRole( + address caller, + uint256 tokenId, + address receiver, + uint96 feeNumerator, + uint256 salePrice + ) + public + { + vm.assume(feeNumerator <= 10000); + vm.assume(receiver != address(0)); + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.assume(salePrice < type(uint128).max); // Buffer for overflow + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.ROYALTY_ADMIN_ROLE()), 32) + ) + ); + vm.prank(caller); + token.setDefaultRoyalty(receiver, feeNumerator); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.ROYALTY_ADMIN_ROLE()), 32) + ) + ); + vm.prank(caller); + token.setTokenRoyalty(tokenId, receiver, feeNumerator); + } + + function boundArrayLength(uint256[] memory arr, uint256 maxSize) private pure returns (uint256[] memory) { + if (arr.length <= maxSize) { + return arr; + } + uint256[] memory result = new uint256[](maxSize); + for (uint256 i; i < maxSize; i++) { + result[i] = arr[i]; + } + return result; + } +} diff --git a/test/tokens/ERC20/ERC20TokenMinter.t.sol b/test/tokens/ERC20/ERC20TokenMinter.t.sol new file mode 100644 index 0000000..60344db --- /dev/null +++ b/test/tokens/ERC20/ERC20TokenMinter.t.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {ERC20TokenMinter} from "src/tokens/ERC20/presets/minter/ERC20TokenMinter.sol"; +import {IERC20TokenMinterSignals} from "src/tokens/ERC20/presets/minter/IERC20TokenMinter.sol"; +import {ERC20TokenMinterFactory} from "src/tokens/ERC20/presets/minter/ERC20TokenMinterFactory.sol"; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +// Interfaces +import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +contract ERC20TokenMinterTest is Test, IERC20TokenMinterSignals { + // Redeclare events + event Transfer(address indexed from, address indexed to, uint256 value); + + ERC20TokenMinter private token; + + uint8 private constant DECIMALS = 18; + + address private proxyOwner; + address private owner; // Token owner + + function setUp() public { + owner = makeAddr("owner"); + proxyOwner = makeAddr("proxyOwner"); + + vm.deal(address(this), 100 ether); + vm.deal(owner, 100 ether); + + ERC20TokenMinterFactory factory = new ERC20TokenMinterFactory(address(this)); + token = ERC20TokenMinter(factory.deploy(proxyOwner, owner, "name", "symbol", DECIMALS)); + } + + function testReinitializeFails() public { + vm.expectRevert(InvalidInitialization.selector); + token.initialize(owner, "name", "symbol", DECIMALS); + } + + function testSupportsInterface() public { + assertTrue(token.supportsInterface(type(IERC165).interfaceId)); + assertTrue(token.supportsInterface(type(IERC20).interfaceId)); + assertTrue(token.supportsInterface(type(IERC20Metadata).interfaceId)); + } + + function testOwnerHasRoles() public { + assertTrue(token.hasRole(token.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(token.hasRole(token.MINTER_ROLE(), owner)); + } + + function testInitValues() public { + assertEq(token.name(), "name"); + assertEq(token.symbol(), "symbol"); + assertEq(token.decimals(), DECIMALS); + } + + // + // Minting + // + function testMintInvalidRole(address caller, uint256 amount) public { + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.assume(amount > 0); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.MINTER_ROLE()), 32) + ) + ); + vm.prank(caller); + token.mint(caller, amount); + } + + function testMintOwner(uint256 amount) public { + vm.assume(amount > 0); + + vm.expectEmit(true, true, true, false, address(token)); + emit Transfer(address(0), owner, amount); + + vm.prank(owner); + token.mint(owner, amount); + + assertEq(token.balanceOf(owner), amount); + } + + function testMintWithRole(address minter, uint256 amount) public { + vm.assume(minter != owner); + vm.assume(minter != proxyOwner); + vm.assume(minter != address(0)); + vm.assume(amount > 0); + // Give role + vm.startPrank(owner); + token.grantRole(token.MINTER_ROLE(), minter); + vm.stopPrank(); + + vm.expectEmit(true, true, true, false, address(token)); + emit Transfer(address(0), owner, amount); + + vm.prank(minter); + token.mint(owner, amount); + + assertEq(token.balanceOf(owner), amount); + } +} diff --git a/test/tokens/ERC721/presets/ERC721Sale.t.sol b/test/tokens/ERC721/presets/ERC721Sale.t.sol new file mode 100644 index 0000000..a86dc2b --- /dev/null +++ b/test/tokens/ERC721/presets/ERC721Sale.t.sol @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {ERC721Sale} from "src/tokens/ERC721/presets/sale/ERC721Sale.sol"; +import {IERC721SaleSignals} from "src/tokens/ERC721/presets/sale/IERC721Sale.sol"; +import {ERC721SaleFactory} from "src/tokens/ERC721/presets/sale/ERC721SaleFactory.sol"; + +import {Merkle} from "murky/Merkle.sol"; +import {ERC20Mock} from "@0xsequence/erc20-meta-token/contracts/mocks/ERC20Mock.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {TestHelper} from "test/tokens/TestHelper.sol"; +import {IMerkleProofSingleUseSignals} from "@0xsequence/contracts-library/tokens/common/IMerkleProofSingleUse.sol"; + +// Interfaces +import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721A} from "erc721a/contracts/interfaces/IERC721A.sol"; +import {IERC721AQueryable} from "erc721a/contracts/interfaces/IERC721AQueryable.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; + +// solhint-disable no-rely-on-time + +contract ERC721SaleTest is Test, Merkle, IERC721SaleSignals, IMerkleProofSingleUseSignals { + // Redeclare events + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + ERC721Sale private token; + ERC20Mock private erc20; + uint256 private perTokenCost = 0.02 ether; + + address private proxyOwner; + + function setUp() public { + proxyOwner = makeAddr("proxyOwner"); + + token = new ERC721Sale(); + token.initialize(address(this), "test", "test", "ipfs://", address(this), 0); + + vm.deal(address(this), 100 ether); + } + + function setUpFromFactory() public { + ERC721SaleFactory factory = new ERC721SaleFactory(address(this)); + token = ERC721Sale(factory.deploy(proxyOwner, address(this), "test", "test", "ipfs://", address(this), 0)); + } + + function testSupportsInterface() public { + assertTrue(token.supportsInterface(type(IERC165).interfaceId)); + assertTrue(token.supportsInterface(type(IERC721).interfaceId)); + assertTrue(token.supportsInterface(type(IERC721A).interfaceId)); + assertTrue(token.supportsInterface(type(IERC721AQueryable).interfaceId)); + assertTrue(token.supportsInterface(type(IAccessControl).interfaceId)); + } + + // + // Minting + // + + // Minting denied when no sale active. + function testMintInactiveFail(bool useFactory, address mintTo, uint256 amount) + public + assumeSafe(mintTo, amount) + withFactory(useFactory) + { + vm.expectRevert(SaleInactive.selector); + token.mint{value: amount * perTokenCost}(mintTo, amount, TestHelper.blankProof()); + } + + // Minting denied when sale is expired. + function testMintExpiredFail(bool useFactory, address mintTo, uint256 amount, uint64 startTime, uint64 endTime) + public + assumeSafe(mintTo, amount) + withFactory(useFactory) + { + if (startTime > endTime) { + uint64 temp = startTime; + startTime = endTime; + endTime = temp; + } + if (block.timestamp >= startTime && block.timestamp <= endTime) { + vm.warp(uint256(endTime) + 1); + } + token.setSaleDetails(0, perTokenCost, address(0), uint64(startTime), uint64(endTime), ""); + + vm.expectRevert(SaleInactive.selector); + token.mint{value: amount * perTokenCost}(mintTo, amount, TestHelper.blankProof()); + } + + // Minting denied when supply exceeded. + function testMintSupplyExceeded(bool useFactory, address mintTo, uint256 amount, uint256 supplyCap) + public + assumeSafe(mintTo, amount) + withFactory(useFactory) + { + if (supplyCap == 0 || supplyCap > 20) { + supplyCap = 1; + } + if (amount <= supplyCap) { + amount = supplyCap + 1; + } + token.setSaleDetails(supplyCap, perTokenCost, address(0), uint64(block.timestamp), uint64(block.timestamp + 1), ""); + + vm.expectRevert(abi.encodeWithSelector(InsufficientSupply.selector, 0, amount, supplyCap)); + token.mint{value: amount * perTokenCost}(mintTo, amount, TestHelper.blankProof()); + } + + // Minting allowed when sale is active. + function testMintSuccess(bool useFactory, address mintTo, uint256 amount) + public + assumeSafe(mintTo, amount) + withFactory(useFactory) + withSaleActive + { + uint256 count = token.balanceOf(mintTo); + vm.expectEmit(true, true, true, true, address(token)); + emit Transfer(address(0), mintTo, 0); + token.mint{value: amount * perTokenCost}(mintTo, amount, TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo)); + } + + // Minting allowed when sale is free. + function testFreeMint(bool useFactory, address mintTo, uint256 amount) + public + assumeSafe(mintTo, amount) + withFactory(useFactory) + { + token.setSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), ""); + + uint256 count = token.balanceOf(mintTo); + vm.expectEmit(true, true, true, true, address(token)); + emit Transfer(address(0), mintTo, 0); + token.mint(mintTo, amount, TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo)); + } + + // Minting allowed when mint charged with ERC20. + function testERC20Mint(bool useFactory, address mintTo, uint256 amount) + public + assumeSafe(mintTo, amount) + withFactory(useFactory) + withERC20 + { + token.setSaleDetails(0, perTokenCost, address(erc20), uint64(block.timestamp - 1), uint64(block.timestamp + 1), ""); + uint256 cost = amount * perTokenCost; + + uint256 balanace = erc20.balanceOf(address(this)); + uint256 count = token.balanceOf(mintTo); + vm.expectEmit(true, true, true, true, address(token)); + emit Transfer(address(0), mintTo, 0); + token.mint(mintTo, amount, TestHelper.blankProof()); + assertEq(count + amount, token.balanceOf(mintTo)); + assertEq(balanace - cost, erc20.balanceOf(address(this))); + assertEq(cost, erc20.balanceOf(address(token))); + } + + // Minting with merkle success. + function testMerkleSuccess(address[] memory allowlist, uint256 senderIndex) + public + { + // Construct a merkle tree with the allowlist. + vm.assume(allowlist.length > 1); + vm.assume(senderIndex < allowlist.length); + bytes32[] memory addrs = new bytes32[](allowlist.length); + for (uint256 i = 0; i < allowlist.length; i++) { + addrs[i] = keccak256(abi.encodePacked(allowlist[i])); + } + bytes32 root = getRoot(addrs); + token.setSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + + bytes32[] memory proof = getProof(addrs, senderIndex); + + address sender = allowlist[senderIndex]; + vm.prank(sender); + token.mint(sender, 1, proof); + + assertEq(1, token.balanceOf(sender)); + } + + // Minting with merkle reuse fail. + function testMerkleReuseFail(address[] memory allowlist, uint256 senderIndex) + public + { + // Copy of testMerkleSuccess + vm.assume(allowlist.length > 1); + vm.assume(senderIndex < allowlist.length); + bytes32[] memory addrs = new bytes32[](allowlist.length); + for (uint256 i = 0; i < allowlist.length; i++) { + addrs[i] = keccak256(abi.encodePacked(allowlist[i])); + } + bytes32 root = getRoot(addrs); + token.setSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + + bytes32[] memory proof = getProof(addrs, senderIndex); + + address sender = allowlist[senderIndex]; + vm.prank(sender); + token.mint(sender, 1, proof); + + assertEq(1, token.balanceOf(sender)); + // End copy + + vm.expectRevert(abi.encodeWithSelector(MerkleProofInvalid.selector, root, proof, sender)); + vm.prank(sender); + token.mint(sender, 1, proof); + } + + // Minting with merkle fail no proof. + function testMerkleFailNoProof(address[] memory allowlist, address sender) + public + { + // Construct a merkle tree with the allowlist. + vm.assume(allowlist.length > 1); + bytes32[] memory addrs = new bytes32[](allowlist.length); + for (uint256 i = 0; i < allowlist.length; i++) { + vm.assume(sender != allowlist[i]); + addrs[i] = keccak256(abi.encodePacked(allowlist[i])); + } + bytes32 root = getRoot(addrs); + token.setSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + + bytes32[] memory proof = TestHelper.blankProof(); + + vm.expectRevert(abi.encodeWithSelector(MerkleProofInvalid.selector, root, proof, sender)); + vm.prank(sender); + token.mint(sender, 1, TestHelper.blankProof()); + } + + // Minting with merkle fail bad proof. + function testMerkleFailBadProof(address[] memory allowlist, address sender) + public + { + // Construct a merkle tree with the allowlist. + vm.assume(allowlist.length > 1); + bytes32[] memory addrs = new bytes32[](allowlist.length); + for (uint256 i = 0; i < allowlist.length; i++) { + vm.assume(sender != allowlist[i]); + addrs[i] = keccak256(abi.encodePacked(allowlist[i])); + } + bytes32 root = getRoot(addrs); + token.setSaleDetails(0, 0, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + + bytes32[] memory proof = getProof(addrs, 1); // Wrong sender + + vm.expectRevert(abi.encodeWithSelector(MerkleProofInvalid.selector, root, proof, sender)); + vm.prank(sender); + token.mint(sender, 1, proof); + } + + // + // Admin minting + // + + // Admin minting denied when not admin. + function testMintAdminFail(address minter, address mintTo, uint256 amount) public assumeSafe(mintTo, amount) { + vm.assume(minter != address(this)); + vm.assume(minter != address(0)); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(minter), + " is missing role ", + Strings.toHexString(uint256(token.MINT_ADMIN_ROLE()), 32) + ) + ); + vm.prank(minter); + token.mintAdmin(mintTo, amount); + } + + // Minting as admin success. + function testMintAdminSuccess(address minter, address mintTo, uint256 amount) public assumeSafe(mintTo, amount) { + token.grantRole(token.MINT_ADMIN_ROLE(), minter); + + uint256 count = token.balanceOf(mintTo); + token.mintAdmin(mintTo, amount); + assertEq(count + amount, token.balanceOf(mintTo)); + } + + // + // Royalty + // + + // Set royalty fails if the caller doesn't have the ROYALTY_ADMIN_ROLE + function testSetRoyaltyFail(address _receiver, uint96 _feeNumerator) public { + token.revokeRole(token.ROYALTY_ADMIN_ROLE(), address(this)); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(address(this)), + " is missing role ", + Strings.toHexString(uint256(token.ROYALTY_ADMIN_ROLE()), 32) + ) + ); + token.setDefaultRoyalty(_receiver, _feeNumerator); + } + + // Test royalty calculation for token with royalty information + function testRoyaltyInfo(uint256 _salePrice, address _receiver, uint96 _feeNumerator) public { + vm.assume(_receiver != address(0)); + vm.assume(_feeNumerator < 10000); + vm.assume(_salePrice < 10000 ether); + + token.setDefaultRoyalty(_receiver, _feeNumerator); + uint256 expectedRoyaltyAmount = (_salePrice * _feeNumerator) / 10000; + + (address receiver, uint256 royaltyAmount) = token.royaltyInfo(0, _salePrice); + + assertEq(_receiver, receiver); + assertEq(expectedRoyaltyAmount, royaltyAmount); + } + + // + // Withdraw + // + + // Withdraw fails if the caller doesn't have the WITHDRAW_ROLE + function testWithdrawFail(address withdrawTo, uint256 amount) public { + token.revokeRole(token.WITHDRAW_ROLE(), address(this)); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(address(this)), + " is missing role ", + Strings.toHexString(uint256(token.WITHDRAW_ROLE()), 32) + ) + ); + token.withdrawETH(withdrawTo, amount); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(address(this)), + " is missing role ", + Strings.toHexString(uint256(token.WITHDRAW_ROLE()), 32) + ) + ); + token.withdrawERC20(address(erc20), withdrawTo, amount); + } + + // Withdraw success ETH + function testWithdrawETH(bool useFactory, address withdrawTo, uint256 amount) public withFactory(useFactory) { + // Address 9 doesnt receive ETH + vm.assume(withdrawTo != address(9)); + testMintSuccess(false, withdrawTo, amount); + + uint256 tokenBalance = address(token).balance; + uint256 balance = withdrawTo.balance; + token.withdrawETH(withdrawTo, tokenBalance); + assertEq(tokenBalance + balance, withdrawTo.balance); + } + + // Withdraw success ERC20 + function testWithdrawERC20(bool useFactory, address withdrawTo, uint256 amount) public withFactory(useFactory) { + testERC20Mint(false, withdrawTo, amount); + + uint256 tokenBalance = erc20.balanceOf(address(token)); + uint256 balance = erc20.balanceOf(withdrawTo); + token.withdrawERC20(address(erc20), withdrawTo, tokenBalance); + assertEq(tokenBalance + balance, erc20.balanceOf(withdrawTo)); + } + + // + // Helpers + // + modifier withFactory(bool useFactory) { + if (useFactory) { + setUpFromFactory(); + } + _; + } + + modifier assumeSafe(address nonContract, uint256 amount) { + vm.assume(uint160(nonContract) > 16); + vm.assume(nonContract.code.length == 0); + vm.assume(amount > 0 && amount < 20); + _; + } + + // Create ERC20. Give this contract 1000 ERC20 tokens. Approve token to spend 100 ERC20 tokens. + modifier withERC20() { + erc20 = new ERC20Mock(); + erc20.mockMint(address(this), 1000 ether); + erc20.approve(address(token), 1000 ether); + _; + } + + modifier withSaleActive() { + token.setSaleDetails(0, perTokenCost, address(0), uint64(block.timestamp - 1), uint64(block.timestamp + 1), ""); + _; + } +} diff --git a/test/tokens/ERC721/presets/ERC721TokenMinter.t.sol b/test/tokens/ERC721/presets/ERC721TokenMinter.t.sol new file mode 100644 index 0000000..783ec2c --- /dev/null +++ b/test/tokens/ERC721/presets/ERC721TokenMinter.t.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {ERC721TokenMinter} from "src/tokens/ERC721/presets/minter/ERC721TokenMinter.sol"; +import {IERC721TokenMinterSignals} from "src/tokens/ERC721/presets/minter/IERC721TokenMinter.sol"; +import {ERC721TokenMinterFactory} from "src/tokens/ERC721/presets/minter/ERC721TokenMinterFactory.sol"; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +// Interfaces +import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; +import {IERC721A} from "erc721a/contracts/interfaces/IERC721A.sol"; +import {IERC721AQueryable} from "erc721a/contracts/extensions/IERC721AQueryable.sol"; + +contract ERC721TokenMinterTest is Test, IERC721TokenMinterSignals { + // Redeclare events + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + ERC721TokenMinter private token; + + address private proxyOwner; + address private owner; + + function setUp() public { + owner = makeAddr("owner"); + proxyOwner = makeAddr("proxyOwner"); + + vm.deal(address(this), 100 ether); + vm.deal(owner, 100 ether); + + ERC721TokenMinterFactory factory = new ERC721TokenMinterFactory(address(this)); + token = ERC721TokenMinter(factory.deploy(proxyOwner, owner, "name", "symbol", "baseURI", address(this), 0)); + } + + function testReinitializeFails() public { + vm.expectRevert(InvalidInitialization.selector); + token.initialize(owner, "name", "symbol", "baseURI", address(this), 0); + } + + function testSupportsInterface() public { + assertTrue(token.supportsInterface(type(IERC165).interfaceId)); + assertTrue(token.supportsInterface(type(IERC721A).interfaceId)); + assertTrue(token.supportsInterface(type(IERC721AQueryable).interfaceId)); + assertTrue(token.supportsInterface(0x80ac58cd)); // ERC721 + assertTrue(token.supportsInterface(0x5b5e139f)); // ERC721Metadata + } + + function testOwnerHasRoles() public { + assertTrue(token.hasRole(token.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(token.hasRole(token.METADATA_ADMIN_ROLE(), owner)); + assertTrue(token.hasRole(token.MINTER_ROLE(), owner)); + assertTrue(token.hasRole(token.ROYALTY_ADMIN_ROLE(), owner)); + } + + function testNameAndSymbol() public { + assertEq(token.name(), "name"); + assertEq(token.symbol(), "symbol"); + } + + // + // Minting + // + function testMintInvalidRole(address caller) public { + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.MINTER_ROLE()), 32) + ) + ); + vm.prank(caller); + token.mint(caller, 1); + } + + function testMintOwner() public { + vm.expectEmit(true, true, true, true, address(token)); + emit Transfer(address(0), owner, 0); + + vm.prank(owner); + token.mint(owner, 1); + + assertEq(token.balanceOf(owner), 1); + } + + function testMintWithRole(address minter) public { + vm.assume(minter != owner); + vm.assume(minter != proxyOwner); + vm.assume(minter != address(0)); + // Give role + vm.startPrank(owner); + token.grantRole(token.MINTER_ROLE(), minter); + vm.stopPrank(); + + vm.expectEmit(true, true, true, true, address(token)); + emit Transfer(address(0), owner, 0); + + vm.prank(minter); + token.mint(owner, 1); + + assertEq(token.balanceOf(owner), 1); + } + + function testMintMultiple() public { + vm.expectEmit(true, true, true, true, address(token)); + emit Transfer(address(0), owner, 0); + vm.expectEmit(true, true, true, true, address(token)); + emit Transfer(address(0), owner, 1); + + vm.prank(owner); + token.mint(owner, 2); + + assertEq(token.balanceOf(owner), 2); + assertEq(token.ownerOf(0), owner); + assertEq(token.ownerOf(1), owner); + } + + // + // Metadata + // + function testMetadataOwner() public { + // Mint token + vm.prank(owner); + token.mint(owner, 2); + + vm.prank(owner); + token.setBaseMetadataURI("ipfs://newURI/"); + + assertEq(token.tokenURI(0), "ipfs://newURI/0"); + assertEq(token.tokenURI(1), "ipfs://newURI/1"); + + // Invalid token + vm.expectRevert(IERC721A.URIQueryForNonexistentToken.selector); + token.tokenURI(2); + } + + function testMetadataInvalid(address caller) public { + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.METADATA_ADMIN_ROLE()), 32) + ) + ); + vm.prank(caller); + token.setBaseMetadataURI("ipfs://newURI/"); + } + + function testMetadataWithRole(address caller) public { + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.assume(caller != address(0)); + // Give role + vm.startPrank(owner); + token.grantRole(token.METADATA_ADMIN_ROLE(), caller); + vm.stopPrank(); + + vm.prank(caller); + token.setBaseMetadataURI("ipfs://newURI/"); + } + + // + // Royalty + // + function testDefaultRoyalty(address receiver, uint96 feeNumerator, uint256 salePrice) public { + vm.assume(feeNumerator <= 10000); + vm.assume(receiver != address(0)); + vm.assume(salePrice < type(uint128).max); // Buffer for overflow + + vm.prank(owner); + token.setDefaultRoyalty(receiver, feeNumerator); + + (address receiver_, uint256 amount) = token.royaltyInfo(1, salePrice); + assertEq(receiver_, receiver); + assertEq(amount, salePrice * feeNumerator / 10000); + } + + function testTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator, uint256 salePrice) public { + vm.assume(feeNumerator <= 10000); + vm.assume(receiver != address(0)); + vm.assume(tokenId != 69); // Other token id for default validation + vm.assume(salePrice < type(uint128).max); // Buffer for overflow + + vm.prank(owner); + token.setTokenRoyalty(tokenId, receiver, feeNumerator); + + (address receiver_, uint256 amount) = token.royaltyInfo(tokenId, salePrice); + assertEq(receiver_, receiver); + assertEq(amount, salePrice * feeNumerator / 10000); + + (receiver_, amount) = token.royaltyInfo(69, salePrice); + assertEq(receiver_, address(this)); + assertEq(amount, 0); + } + + function testRoyaltyWithRole( + address caller, + uint256 tokenId, + address receiver, + uint96 feeNumerator, + uint256 salePrice + ) + public + { + vm.assume(feeNumerator <= 10000); + vm.assume(receiver != address(0)); + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.assume(salePrice < type(uint128).max); // Buffer for overflow + + vm.startPrank(owner); + token.grantRole(token.ROYALTY_ADMIN_ROLE(), caller); + vm.stopPrank(); + + vm.prank(caller); + token.setDefaultRoyalty(receiver, feeNumerator); + + (address receiver_, uint256 amount) = token.royaltyInfo(1, salePrice); + assertEq(receiver_, receiver); + assertEq(amount, salePrice * feeNumerator / 10000); + + vm.prank(caller); + token.setTokenRoyalty(tokenId, receiver, feeNumerator); + + (receiver_, amount) = token.royaltyInfo(tokenId, salePrice); + assertEq(receiver_, receiver); + assertEq(amount, salePrice * feeNumerator / 10000); + } + + function testRoyaltyInvalidRole( + address caller, + uint256 tokenId, + address receiver, + uint96 feeNumerator, + uint256 salePrice + ) + public + { + vm.assume(feeNumerator <= 10000); + vm.assume(receiver != address(0)); + vm.assume(caller != owner); + vm.assume(caller != proxyOwner); + vm.assume(salePrice < type(uint128).max); // Buffer for overflow + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.ROYALTY_ADMIN_ROLE()), 32) + ) + ); + vm.prank(caller); + token.setDefaultRoyalty(receiver, feeNumerator); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(caller), + " is missing role ", + Strings.toHexString(uint256(token.ROYALTY_ADMIN_ROLE()), 32) + ) + ); + vm.prank(caller); + token.setTokenRoyalty(tokenId, receiver, feeNumerator); + } +} diff --git a/test/tokens/TestHelper.sol b/test/tokens/TestHelper.sol new file mode 100644 index 0000000..8235385 --- /dev/null +++ b/test/tokens/TestHelper.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +library TestHelper { + + function singleToArray(uint256 value) internal pure returns (uint256[] memory) { + uint256[] memory values = new uint256[](1); + values[0] = value; + return values; + } + + function blankProof() internal pure returns (bytes32[] memory) { + return new bytes32[](0); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0476128 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "include": ["./scripts"], + "compilerOptions": { + "module": "esnext", + "lib": ["esnext"], + "importHelpers": true, + "declaration": true, + "sourceMap": true, + "rootDir": "./", + "baseUrl": "./", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "target": "ES6" + }, + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..6e430d7 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1879 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@0xsequence/erc-1155@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@0xsequence/erc-1155/-/erc-1155-4.0.1.tgz#c991fe06b6ae385146e69cd9d1c4982b06487c10" + integrity sha512-KFLxBfiocOuHmPUkGYiWw5fLZ8uCDhXhcyzFFv8oe/KWXdxL37NTD7n6CmMSRiUxr4qaXuFV5u38vPFWFdOY0g== + optionalDependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/providers" "^5.7.2" + ethers "^5.7.2" + +"@0xsequence/erc-1155@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@0xsequence/erc-1155/-/erc-1155-4.0.3.tgz#b9b890c1ced04a84f36be22dd20424b43b9e1da2" + integrity sha512-vDh4OEuq0bR3iIqhsxpuRpczH5GxwH/mjBWm9uP6VYKnKM36ZBLApSNUlIOCXYRCS0DYFxsdvYjAemb3w1l4ow== + optionalDependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/providers" "^5.7.2" + ethers "^5.7.2" + +"@0xsequence/erc20-meta-token@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@0xsequence/erc20-meta-token/-/erc20-meta-token-4.0.1.tgz#44fe77822af0ff3b1111466615c9958e92aca782" + integrity sha512-q3yIR5OwsTK+HnTQVXTDlknMo2kE65rBwZN6ymPeoe1CW+RE9XqUM3QCvxTlLGuAnsIaL6/ABG2ePX4crMvbaw== + dependencies: + "@0xsequence/erc-1155" "^4.0.1" + +"@babel/code-frame@^7.0.0": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" + integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" + integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" + integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + +"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" + integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + +"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" + integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + +"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" + integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" + integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + +"@ethersproject/contracts@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" + integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== + dependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + +"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" + integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf" + integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + +"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360" + integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + aes-js "3.0.0" + scrypt-js "3.0.1" + +"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + +"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + +"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" + integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102" + integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + +"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" + integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" + integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + bech32 "1.1.4" + ws "7.4.6" + +"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" + integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" + integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + hash.js "1.1.7" + +"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" + integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + +"@ethersproject/solidity@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" + integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" + integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" + integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + +"@ethersproject/units@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1" + integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/wallet@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d" + integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/json-wallets" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + +"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" + integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== + dependencies: + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5" + integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@openzeppelin/contracts@^4.8.3": + version "4.8.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.3.tgz#cbef3146bfc570849405f59cba18235da95a252a" + integrity sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg== + +"@solidity-parser/parser@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.0.tgz#1fb418c816ca1fc3a1e94b08bcfe623ec4e1add4" + integrity sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q== + dependencies: + antlr4ts "^0.5.0-alpha.4" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/bn.js@^5.1.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" + integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.3.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.2.tgz#fa6a90f2600e052a03c18b8cb3fd83dd4e599898" + integrity sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw== + +"@types/node@^20.1.0": + version "20.1.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.0.tgz#258805edc37c327cf706e64c6957f241ca4c4c20" + integrity sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A== + +"@types/pbkdf2@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/pbkdf2/-/pbkdf2-3.1.0.tgz#039a0e9b67da0cdc4ee5dab865caa6b267bb66b1" + integrity sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ== + dependencies: + "@types/node" "*" + +"@types/secp256k1@^4.0.1": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.3.tgz#1b8e55d8e00f08ee7220b4d59a6abe89c37a901c" + integrity sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w== + dependencies: + "@types/node" "*" + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +aes-js@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^6.12.6: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +antlr4@^4.11.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.0.tgz#25c0b17f0d9216de114303d38bafd6f181d5447f" + integrity sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew== + +antlr4ts@^0.5.0-alpha.4: + version "0.5.0-alpha.4" + resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" + integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +ast-parents@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/ast-parents/-/ast-parents-0.0.1.tgz#508fd0f05d0c48775d9eccda2e174423261e8dd3" + integrity sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^3.0.2: + version "3.0.9" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" + integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== + dependencies: + safe-buffer "^5.0.1" + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bech32@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + +bignumber.js@^9.0.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" + integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== + +blakejs@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" + integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== + +bn.js@4.11.6: + version "4.11.6" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" + integrity sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA== + +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browserify-aes@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +bs58check@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" + integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== + dependencies: + bs58 "^4.0.0" + create-hash "^1.1.0" + safe-buffer "^5.1.2" + +buffer-reverse@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-reverse/-/buffer-reverse-1.0.1.tgz#49283c8efa6f901bc01fa3304d06027971ae2f60" + integrity sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" + integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + +cli-truncate@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" + integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== + dependencies: + slice-ansi "^5.0.0" + string-width "^5.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.19: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +cosmiconfig@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" + integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-js@^3.1.9-1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.3.0.tgz#846dd1cce2f68aacfa156c8578f926a609b7976b" + integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q== + +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dotenv@^16.1.4: + version "16.1.4" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.1.4.tgz#67ac1a10cd9c25f5ba604e4e08bc77c0ebe0ca8c" + integrity sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +elliptic@6.5.4, elliptic@^6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +erc721a-upgradeable@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/erc721a-upgradeable/-/erc721a-upgradeable-4.2.3.tgz#9e67a9d628f8648a0cd64d17a7a13eeba61deba1" + integrity sha512-EaHbOVDau9drDNpi/gWUHHaopCh35NMATa+3+9ZmdHokw9kfPiDD5RhGRlXA1aZA0ZfYvqPEbaKuSH3PaCY2Ug== + +erc721a@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/erc721a/-/erc721a-4.2.3.tgz#ca6469b0e54afb0f614272c2147dc4cb49ff223f" + integrity sha512-0deF0hOOK1XI1Vxv3NKDh2E9sgzRlENuOoexjXRJIRfYCsLlqi9ejl2RF6Wcd9HfH0ldqC03wleQ2WDjxoOUvA== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +ethereum-bloom-filters@^1.0.6: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz#3ca07f4aed698e75bd134584850260246a5fed8a" + integrity sha512-rxJ5OFN3RwjQxDcFP2Z5+Q9ho4eIdEmSc2ht0fCu8Se9nbXjZ7/031uXoUYJ87KHCOdVeiUuwSnoS7hmYAGVHA== + dependencies: + js-sha3 "^0.8.0" + +ethereum-cryptography@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191" + integrity sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ== + dependencies: + "@types/pbkdf2" "^3.0.0" + "@types/secp256k1" "^4.0.1" + blakejs "^1.1.0" + browserify-aes "^1.2.0" + bs58check "^2.1.2" + create-hash "^1.2.0" + create-hmac "^1.1.7" + hash.js "^1.1.7" + keccak "^3.0.0" + pbkdf2 "^3.0.17" + randombytes "^2.1.0" + safe-buffer "^5.1.2" + scrypt-js "^3.0.0" + secp256k1 "^4.0.1" + setimmediate "^1.0.5" + +ethereumjs-util@^7.1.0: + version "7.1.5" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181" + integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg== + dependencies: + "@types/bn.js" "^5.1.0" + bn.js "^5.1.2" + create-hash "^1.1.2" + ethereum-cryptography "^0.1.3" + rlp "^2.2.4" + +ethers@^5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" + integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== + dependencies: + "@ethersproject/abi" "5.7.0" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/address" "5.7.0" + "@ethersproject/base64" "5.7.0" + "@ethersproject/basex" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@ethersproject/bytes" "5.7.0" + "@ethersproject/constants" "5.7.0" + "@ethersproject/contracts" "5.7.0" + "@ethersproject/hash" "5.7.0" + "@ethersproject/hdnode" "5.7.0" + "@ethersproject/json-wallets" "5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/logger" "5.7.0" + "@ethersproject/networks" "5.7.1" + "@ethersproject/pbkdf2" "5.7.0" + "@ethersproject/properties" "5.7.0" + "@ethersproject/providers" "5.7.2" + "@ethersproject/random" "5.7.0" + "@ethersproject/rlp" "5.7.0" + "@ethersproject/sha2" "5.7.0" + "@ethersproject/signing-key" "5.7.0" + "@ethersproject/solidity" "5.7.0" + "@ethersproject/strings" "5.7.0" + "@ethersproject/transactions" "5.7.0" + "@ethersproject/units" "5.7.0" + "@ethersproject/wallet" "5.7.0" + "@ethersproject/web" "5.7.1" + "@ethersproject/wordlists" "5.7.0" + +ethjs-unit@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699" + integrity sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw== + dependencies: + bn.js "4.11.6" + number-to-bn "1.7.0" + +evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" + integrity sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.1" + human-signals "^4.3.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^3.0.7" + strip-final-newline "^3.0.0" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +human-signals@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" + integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== + +husky@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" + integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-hex-prefixed@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554" + integrity sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-sha3@0.8.0, js-sha3@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +keccak256@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.6.tgz#dd32fb771558fed51ce4e45a035ae7515573da58" + integrity sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw== + dependencies: + bn.js "^5.2.0" + buffer "^6.0.3" + keccak "^3.0.2" + +keccak@^3.0.0, keccak@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.3.tgz#4bc35ad917be1ef54ff246f904c2bbbf9ac61276" + integrity sha512-JZrLIAJWuZxKbCilMpNz5Vj7Vtb4scDG3dMXLOsbzBmQGyjwE61BbW7bJkfKKCShXiQZt3T6sBgALRtmd+nZaQ== + dependencies: + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + readable-stream "^3.6.0" + +lilconfig@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lint-staged@^13.2.2: + version "13.2.2" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.2.2.tgz#5e711d3139c234f73402177be2f8dd312e6508ca" + integrity sha512-71gSwXKy649VrSU09s10uAT0rWCcY3aewhMaHyl2N84oBk4Xs9HgxvUp3AYu+bNsK4NrOYYxvSgg7FyGJ+jGcA== + dependencies: + chalk "5.2.0" + cli-truncate "^3.1.0" + commander "^10.0.0" + debug "^4.3.4" + execa "^7.0.0" + lilconfig "2.1.0" + listr2 "^5.0.7" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-inspect "^1.12.3" + pidtree "^0.6.0" + string-argv "^0.3.1" + yaml "^2.2.2" + +listr2@^5.0.7: + version "5.0.8" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-5.0.8.tgz#a9379ffeb4bd83a68931a65fb223a11510d6ba23" + integrity sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.19" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.8.0" + through "^2.3.8" + wrap-ansi "^7.0.0" + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merkletreejs@^0.2.32: + version "0.2.32" + resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.2.32.tgz#cf1c0760e2904e4a1cc269108d6009459fd06223" + integrity sha512-TostQBiwYRIwSE5++jGmacu3ODcKAgqb0Y/pnIohXS7sWxh1gCkSptbmF1a43faehRDpcHf7J/kv0Ml2D/zblQ== + dependencies: + bignumber.js "^9.0.1" + buffer-reverse "^1.0.1" + crypto-js "^3.1.9-1" + treeify "^1.1.0" + web3-utils "^1.3.4" + +micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +node-addon-api@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" + integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== + +node-gyp-build@^4.2.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" + integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" + integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== + dependencies: + path-key "^4.0.0" + +number-to-bn@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/number-to-bn/-/number-to-bn-1.7.0.tgz#bb3623592f7e5f9e0030b1977bd41a0c53fe1ea0" + integrity sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig== + dependencies: + bn.js "4.11.6" + strip-hex-prefix "1.0.0" + +object-inspect@^1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pbkdf2@^3.0.17: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pidtree@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +prettier@^2.8.3: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rlp@^2.2.4: + version "2.2.7" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf" + integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== + dependencies: + bn.js "^5.2.0" + +rxjs@^7.8.0: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +scrypt-js@3.0.1, scrypt-js@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" + integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== + +secp256k1@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" + integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.2, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +solhint@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.4.1.tgz#8ea15b21c13d1be0b53fd46d605a24d0b36a0c46" + integrity sha512-pzZn2RlZhws1XwvLPVSsxfHrwsteFf5eySOhpAytzXwKQYbTCJV6z8EevYDiSVKMpWrvbKpEtJ055CuEmzp4Xg== + dependencies: + "@solidity-parser/parser" "^0.16.0" + ajv "^6.12.6" + antlr4 "^4.11.0" + ast-parents "^0.0.1" + chalk "^4.1.2" + commander "^10.0.0" + cosmiconfig "^8.0.0" + fast-diff "^1.2.0" + glob "^8.0.3" + ignore "^5.2.4" + js-yaml "^4.1.0" + lodash "^4.17.21" + pluralize "^8.0.0" + semver "^6.3.0" + strip-ansi "^6.0.1" + table "^6.8.1" + text-table "^0.2.0" + optionalDependencies: + prettier "^2.8.3" + +string-argv@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" + integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== + dependencies: + ansi-regex "^6.0.1" + +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + +strip-hex-prefix@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f" + integrity sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A== + dependencies: + is-hex-prefixed "1.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +table@^6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" + integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +treeify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" + integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utf8@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +web3-utils@^1.3.4: + version "1.10.0" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.0.tgz#ca4c1b431a765c14ac7f773e92e0fd9377ccf578" + integrity sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg== + dependencies: + bn.js "^5.2.1" + ethereum-bloom-filters "^1.0.6" + ethereumjs-util "^7.1.0" + ethjs-unit "0.1.6" + number-to-bn "1.7.0" + randombytes "^2.1.0" + utf8 "3.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + +yaml@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" + integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==